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
LaTeX/Éléments de base
0
542
772927
761364
2026-09-23T07:50:46Z
Cdang
1202
/* Espaces et changements de ligne */ accord
772927
wikitext
text/x-wiki
<noinclude>{{NavTitre|book={{BASEPAGENAME}}|prev=Lettre|next=Les classes}}</noinclude>
<noinclude>{{LaTeX/Navigation}}</noinclude>
== Bloc ==
Dans LaTeX, un bloc est une portion de texte comprise entre deux accolades « {…} ». Souvent, une instruction agit sur un bloc, par exemple :
<syntaxhighlight lang="latex">
\emph{texte}
</syntaxhighlight>
ou bien un bloc sert à limiter la zone sur laquelle agit une instruction de portée « infinie », par exemple
<syntaxhighlight lang="latex">
{\em texte}
</syntaxhighlight>
On utilisera parfois le bloc vide
<syntaxhighlight lang="latex">
{}
</syntaxhighlight>
: Les accolades n'apparaissent pas sur le rendu final
(voir la section suivante pour mettre des accolades dans le texte final).
== Caractères réservés ==
Dans le premier exemple, nous avons vu que la contre-oblique « \ » servait à indiquer les instructions, les accolades « {} » un bloc et que le tilde « ~ » indiquait l'espace insécable. Si nous voulons faire figurer ces caractères dans le document final, on ne peut donc pas les taper tels quels dans le fichier source. Il existe d'autres caractères réservés, en voici la liste :
{ } % # $ ^ ~ & _ \
l'avant-dernier caractère est le tiret de soulignement ou ''underscore'' (touche 8 sur un clavier de PC, ou touche à droite de la parenthèse fermante sur un clavier Mac).
Si nous voulons utiliser ces caractères, il faudra entrer :
* <code>\{</code> pour {
* <code>\}</code> pour }
* <code>\%</code> pour %
* <code>\#</code> pour #
* <code>\$</code> pour $
* <code>\textasciicircum</code> pour ^ (mnémotechnique anglais : ''text, ASCII, circumflex'') ;
* <code>\textasciitilde</code> pour ~ (mnémotechnique anglais : ''text, ASCII, tilde'') ;
* <code>\&</code> pour &
* <code>\_</code> pour _
* <code>\textbackslash</code> pour \ (mnémotechnique anglais : ''text, backslash'').
Les commandes avec un caractère réservé peuvent être suivies par n'importe quoi, il n'y a pas d'ambiguïté. Par contre, les commandes constituées de lettres ne peuvent pas être suivies de lettres : comment savoir où s'arrête la commande ? Elles peuvent par contre être suivies d'un signe autre qu'une lettre : un nombre, un signe de ponctuation…
Si l'on veut, dans le document final, accoler une lettre au caractère obtenu avec une telle commande, il faut indiquer la fin de la commande :
* soit laisser une espace après la commande ; cette espace sera ignorée pour le rendu final ;
* soit placer une paire d'accolades vides <code>{}</code>.
Par exemple
* <code>\textasciicircumA</code> génère une erreur ;
* <code>\textasciicircum˽A</code> et <code>\textasciicircum{}A</code> donnent « ^A » ;
* <code>\textasciicircum{}˽A</code> donne « ^ A ».
Le caractère « ˽ » sert à indiquer l'espace, pour clarifier les choses. On note ici que le fait que le fichier contienne un caractère espace (obtenu avec la barre d'espacement du clavier) ne signifie pas que le document final aura une espace<ref>en typographie le mot espace est au féminin lorsqu'il désigne un écart horizontal</ref>.
== Diacritiques et ligatures ==
Au chapitre ''premier exemple'', nous avons vu qu'avec l'extension <code>inputenc</code>, nous pouvions choisir le jeu de caractères et donc utiliser les caractères accentués, les caractères de la langue française et de manière plus spécifique [[w:Diacritique|diacritiqués]], du clavier. Le problème se pose lorsque le clavier ne permet pas de faire les caractères que nous voulons, par exemple :
* les capitales accentuées<ref>en bonne typographie, on met les accents sur les capitales. Notez que certains systèmes (comme GNU/Linux) gèrent nativement les majuscules accentuées. Pour cela, activez le verrouillage majuscule (touche au-dessus du Shift) et appuyez sur la touche accentuée concernée.</ref> comme « É » ;
* les lettres des langues étrangères au clavier : accents français avec un clavier anglais, lettres danoises avec un clavier français, …
Voici quelques exemples :
* <code>\'e</code> ou <code>\'{e}</code> pour é ;
* <code>\`e</code> ou <code>\`{e}</code> pour è ;
* <code>\^\i</code> ou <code>\^{\i}</code> pour î ;
* <code>\`A</code> ou <code>\`{A}</code> pour À ;
* <code>\"e</code> ou <code>\"{e}</code> pour ë ;
* <code>\c˽c</code> ou <code>\c{c}</code> pour ç,
et ainsi de suite.
On remarque que :
* le diacritique est une commande qui est suivie directement par la lettre :
** <code>\'</code> (contre-oblique et apostrophe) pour l'accent aigu,
** <code>\`</code> (contre-oblique et apostrophe inversée) pour l'accent grave,
** <code>\^</code> (contre-oblique et chapeau) pour l'accent circonflexe,
** <code>\"</code> (contre-oblique et guillemet double anglais) pour le tréma,
** <code>\c</code> (contre-oblique et c) pour une cédille ; la lettre cédillée doit être mise entre accolades, ou bien séparée d'une espace ;
* pour mettre un accent sur le i minuscule, il faut utiliser <code>\i</code> (contre-oblique, i), afin de supprimer le point sur le i avant de placer l'accent.
On peut aussi utiliser les ligatures :
* <code>\oe</code> pour œ ;
* <code>\OE</code> pour Œ ;
* <code>\ae</code> pour æ ;
* <code>\AE</code> pour Æ.
Les physiciens pourront avoir recours à <code>\AA</code> pour Å.
Le problème des commandes composées de lettres (comme <code>\c</code>, <code>\i</code>, <code>\oe</code> et <code>\AA</code>) est le même que ci-dessus. Par exemple, il faut taper
* <code>c\oe˽ur</code> ou <code>c\oe{}ur</code> pour obtenir « cœur » ;
* <code>fa\^\i˽t</code> ou <code>fa\^\i{}t</code> pour obtenir « faît » ;
* <code>\c˽ca</code> ou <code>\c{c}a</code> pour obtenir « ça ».
Cette précaution est par contre inutile si le caractère suivant n'est pas une lettre. Comment alors indiquer que l'on veut une espace après ? Deux solutions :
* soit on indique explicitement la présence d'une espace dite « justifiante » (c'est-à-dire non insécable et de dimension variable pour s'ajuster à la longueur de la ligne et au nombre de caractères), avec la commande <code>\˽</code> (contre-oblique, espace) ;
* soit on indique la fin de la commande avec des accolades vides <code>{}</code>, le tout suivi d'une espace.
Par exemple :
<syntaxhighlight lang="tex">
On peut obtenir \OE\ avec \textbackslash OE, et \AE{} avec \textbackslash{}AE.
Le symbole de l'angstr\"om est \AA.
</syntaxhighlight>
donne
{{début rendu LaTeX}}
On peut obtenir Œ avec \OE, et Æ avec \AE. Le symbole de l'angström est Å.
{{fin rendu LaTeX}}
== Quelques autres caractères ==
Voici quelques autres caractères que l'on peut obtenir.
{| class="wikitable"
|+ Caractères LaTeX
|-
! scope="col" | Saisie
! scope="col" | Caractère
! scope="col" | Note
|-
| <code>\copyright</code> || © || ''{{lang|en|copyright}}''
|-
| <code>\dag</code> || † || obèle
|-
| <code>\ddag</code> || ‡ || double obèle
|-
| <code>\dots</code> || … || points de suspension (plus espacés que trois points)
|-
| <code>\o</code> || ø || o barré
|-
| <code>\O</code> || Ø ||notation de l'ensemble vide (mathématiques)
|-
| <code>\P</code> || ¶ || pied-de-mouche
|-
| <code>\pounds</code> || £ || livre sterling
|-
| <code>\S</code> || § || paragraphe
|-
| <code>\ss</code> || ß || eszett (lettre allemande)
|-
| <code>\textbar</code> || <nowiki>|</nowiki> || tube
|-
| <code>\textperiodcentered</code> || · || point centré
|-
| <code>\textregistered</code> || ® || ''{{lang|en|registred}}''
|-
| <code>\texttrademark</code> || ™ || ''{{lang|en|trade mark}}''
|-
| <code>\textvisiblespace</code> || ˽ || matérialisation d'une espace
|}
Certains caractères ont une saisie simplifiée.
{| class="wikitable"
|+ Caractères LaTeX
|-
! scope="col" | Saisie
! scope="col" | Caractère
! scope="col" | Note
|-
| <code>?`</code> || ¿ || « ` » est l'apostrophe inversée (accent grave) ; alternative : <code>\textquestiondown</code>
|-
| <code>!`</code> || ¡ || ''idem'' ; alternative : <code>\textexclamdown</code>
|-
| <code>-</code> || - || division (trait d'union)
|-
| <code>--</code> || – || tiret demi-cadratin, alternative : <code>\endash</code>
|-
| <code>---</code> || — || tiret cadratin, alternative : <code>\emdash</code>
|-
| <code id="guillemets1"><<</code> || « || guillemet français ouvrant, avec le codage de polices T1 mais [[#extensionfrancaise|voir ci-dessous]]
|-
| <code>>></code> || » || guillemet français fermant, avec le codage de polices T1 mais [[#extensionfrancaise|voir ci-dessous]]
|}
Les symboles ci-dessus peuvent aussi être mis directement dans le fichier <code>.tex</code> si vous utilisez un codage adéquat, comme l'ISO Latin 1 ou l'UTF8 (cf. ''[[../Premier exemple#Améliorations du code source|Premier exemple > Améliorations du code source]]''). Notez que l'utilisation des guillemets français tels quels ne provoque pas un affichage correct des espaces en français, dans le cas d'une citation, il faut donc utiliser <code>\og</code> et <code>\fg</code> à la place (voir ci-après).
L'euro (la monnaie) a été introduite récemment par rapport à l'histoire de LaTeX. Elle n'est donc pas disponible dans les fontes de base. Pour l'obtenir, il faut :
* charger l'extension <code>eurosym</code>, en mettant <code>\usepackage{eurosym}</code> dans le préambule ;
* dans le texte, utiliser la commande <code>\euro</code> (éventuellement <code>\euro{}</code>).
=== Cas du point d'abréviation anglais ===
En typographie anglaise, l'espace après un point final de phrase est plus grande que l'espace après un point d'abréviation. Par défaut, LaTeX considère qu'un point est un point final, mais il peut reconnaître dans certains cas les points d'abréviation (comme par exemple dans <code>U.F.O.</code>). Si l'on veut forcer un point d'abréviation, il faut utiliser <code>.\@</code> (point, contre-oblique, arobase).
; Exemple
<syntaxhighlight lang="tex">
U.F.O.\@ means ``undentified flying object''.
</syntaxhighlight>
Dans certains cas, il vaut mieux utiliser le tilde, espace insécable, qui permet d'avoir le bon espacement ''et'' de ne pas avoir de coupure en fin de ligne :
; Exemple
<syntaxhighlight lang="tex">
And here's to you Mrs.~Robinson
</syntaxhighlight>
À l'inverse, si l'on veut forcer un point de fin de ligne (par exemple phrase se terminant par une abréviation, le dernier point d'abréviation étant aussi point de fin de phrase), on peut écrire <code>\@.</code>, ou bien mettre le mot entre accolade.
; Exemple
<syntaxhighlight lang="tex">
This is a U.F.O\@.
</syntaxhighlight>
: ou bien
<syntaxhighlight lang="tex">
This is a {U.F.O.}
</syntaxhighlight>
Tout ceci ne s'applique pas en typographie française (l'espace est identique quel que soit le type de point), et l'extension <code>babel</code> ajuste automatiquement la taille de l'espace après un point (voir plus loin).
=== Autres langues ===
Voici quelques diacritiques (accents) utilisés dans d'autres langues que le français, telles que le roumain :
{|class="wikitable"
|+ Diacritiques en langues autres que le français
! scope="col" | Saisie
! scope="col" | Caractère
! scope="col" | Note
|-
| <code>\H{o}</code> || ő || double accent aigu : tréma long hongrois
|-
| <code>\~{n}</code> || ñ || tilde : espagnol, estonien, vietnamien, võro
|-
| <code>\k{a}</code> || ą || ogonek : polonais, lituanien
|-
| <code>\l{}</code> || ł || l barré : polonais)
|-
| <code>\={o}</code> || ō || macron : en français, accent « plat, neutre » (lorsque l'on hésite entre l'accent grave et l'aigu ou bien sur des capitales pour des raisons d'esthétique), transcription de l'arabe, du japonais (méthode Hepburn) ou du cantonnais (hanyu pinyin) ; letton, lituanien, latin (en typographie moderne, en remplacement de l'accent aigu)
|-
| <code>\b{o}</code> || <u>o</u> || trait souscrit ou soulignement : langues du Gabon, shoshone, transcription du dakelh
|-
| <code>\.{o}</code> || ȯ || point suscrit ou point en chef : écriture onciale (Moyen-Âge), vieil anglais, vieil irlandais, maltais, turc, polonais, lituanien, cheyenne, transcriptions du sanskrit, de l'arabe, du kazakh, du tadjik, du kurde, du livonien, du tchétchène, du venda, de l'igbo, de l'arménien, de l'ulithien et du basa ; en physique, indique la dérivation par rapport eu temps
|-
| <code>\d{u}</code> || ụ || point souscrit : asturien, vietnamien ''({{lang|vi|quốc ngữ}})'', allemand (dans les dictionnaires), transcription de langues indiennes ou afro-asiatiques, transcription de lettre incertaines en archéologie (épigraphie) ;
|-
| <code>\r{a}</code> || å || rond en chef, anneau en chef : suédois, danois, norvégien, tchèque, lituanien, wallon, picard, alphabet phonétique international (API)
|-
| <code>\u{o}</code> || ŏ || brève : alphabet cyrillique (biélorusse, russe, ukrainien, langues caucasiennes et turques, tchouvache), esperanto, roumain, turc, vietnamien ''({{lang|vi|quốc ngữ}})''
|-
| <code>\v{s}</code> || š || caron ou hatchek (háček), antiflexe, inflexe, circonflexe inversé ou chevron : slovène, tchèque, slovaque, croate, finnois, écriture latine du bosnien, du serbe et du tamazight, transcription du bulgare et du macédonien
|-
| <code>\t{oo}</code> || o͡o || tirant en chef, tirant suscrit, double brève, dos-d'âne, trait d’union papyrologique : alphabet phonétique international (API), alphabet phonétique ouralien (APO), transcription du cyrillique (méthode ALA-LC), notation Z (modélisation informatique), musique ''({{lang|it|legato}})''
|-
|<code>\cb{s}</code> || ș || La virgule souscrite peut être obtenue en utilisant <code>\cb{s}</code> du paquet <code>combelow</code> : roumain
|}
== <span id="extensionfrancaise">Extension française</span> ==
L'extension française utilisée fournit des commandes supplémentaires, facilitant l'édition.
Voici quelques exemples :
; Commandes communes à <code id="babel2">babel</code> option <code>french</code> et à <code>frenchle</code>
* <code id="guillemets2">\og</code> : « suivi d'une espace insécable ;
* <code>\fg</code> : » précédé d'une espace insécable ;
Si le caractère qui suit le guillemet fermant est un espace, il faut utiliser soit <code>\fg{} </code>, soit <code>\fg\˽</code>.
; Commandes de <code>babel</code> option <code>french</code>
* <code>\degres</code> : ° ;
* <code>\ier</code> : <sup>er</sup> ;
* <code>\iere</code> : <sup>re</sup> ;
* <code>\iers</code> : <sup>ers</sup> ;
* <code>\ieres</code> : <sup>res</sup> ;
* <code>\ieme</code> : <sup>e</sup> ;
* <code>\iemes</code> : <sup>es</sup> ;
* <code>\bsc{…}</code> : petites capitales, le mot n'étant pas césuré.
== Espaces et changements de ligne ==
On remarque dans l'exemple ci-dessus que le fait que l'on ait changé de ligne dans le fichier source ne provoque pas de changement de ligne dans le résultat final. Le changement de ligne simple équivaut à un espace. Par contre, si on laisse une ou plusieurs lignes vides, cela indique un changement de paragraphe, caractérisé par un retour à la ligne et :
* soit un alinéa rentrant (indentation), qui est le cas général en typographie française et anglaise ;
* soit par un interligne plus grand, solution souvent retenue pour des ouvrages techniques et popularisée par les navigateurs Web et le traitement de texte Microsoft Word, pour lesquels c'est la mise en page par défaut.
On peut laisser plusieurs lignes vides, cela équivaut à une seule ligne vide.
De même, si l'on met plusieurs espaces, c'est comme s'il n'y en avait qu'un seul.
Concernant les blancs entre les mots (espaces, au féminin), il existe plusieurs types d'espaces :
* espace justifiante (de dimension variable) : obtenu par une ou plusieurs pressions sur la barre d'espacement et/ou une pression sur la touche {{touche|Entrée}}, ou bien par <code>\˽</code> ;
* espace insécable : <code>~</code> (tilde) ;
* petite espace : <code>\,</code> ;
* espace fine : <code>\/</code> : espace encore plus petite, permettant de régler des problèmes de débordement de l'italique (<code>\emph{…\/}</code>), ou bien d'empêcher les ligatures.
Pour introduire un espace horizontal d'une longueur donnée, on utilise la commande <code>\hspace{''longueur''}</code> où ''longueur'' est un nombre avec une unité accolée :
* <code>mm</code> (millimètre) ;
* <code>cm</code> (centimètre) ;
* <code>pt</code> (point anglo-saxon) ;
* <code>dd</code> (point Didot) ;
* <code>ex</code> (hauteur d'x) ;
* <code>em</code> (cadratin).
Les unités <code>ex</code> et <code>em</code> sont proportionnelles au corps de la police :
* la hauteur d'x, parfois appelée à tort « hauteur d'œil », est la hauteur d'un bas de casse (lettre minuscule) sans hampe ni jambage, comme le ''x'' ;
* le cadratin est égal au corps de la police.
Par exemple :
: <code>\hspace{1cm}</code> pour un espace de un centimètre ;
: <code>\hspace{0,5em}</code> pour un espace d'un demi cadratin.
On peut utiliser un point ou une virgule comme séparateur décimal.
La commande <code>\hfill</code> introduit un espace « ressort » : il pousse ce qu'il y a à gauche et à droite pour occuper tout l'espace restant sur la ligne. S'il y a plusieurs commandes <code>\hfill</code> sur la même ligne, les espaces sont de même largeur.
De même, si l'on veut espacer des paragraphes, on utilise la commande <code>\vspace{''longueur''}</code>. On peut également utiliser les commandes :
* <code>\medskip</code> pour sauter une ligne « normale » ;
* <code>\smallskip</code> pour un « petit » saut de ligne ;
* <code>\bigskip</code> pour un « grand » saut de ligne.
La commande <code>\vfill</code> introduit un espace « ressort » : il pousse ce qu'il y a au dessus et en dessous pour occuper tout l'espace restant sur la page. S'il y a plusieurs commandes <code>\vfill</code> sur la même page, les espaces sont de même hauteur.
=== Structuration du code source ===
Comme pour tout code source de programme, on a intérêt à structurer le fichier LaTeX afin de retrouver facilement les passages que l'on veut.
LaTeX n'étant pas WYSIWYG, on peut organiser le texte comme l'on veut, ce qui facilite sa lecture à l'écran ; en particulier, on peut introduire un retour de ligne entre chaque portion de phrase, entre chaque proposition. Le fait de pouvoir sauter des lignes entre les paragraphes et mettre des espaces ou tabulations à profusion en début de ligne permet de donner une « forme » au code qui rend son analyse facile (notion d'indentation).
Par ailleurs, on peut introduire des commentaires en utilisant le signe <code>%</code>.
Notons que les commentaires permettent aussi :
* d'effacer une portion de texte sans la perdre : on met les lignes en commentaires (on place un « % » à leur début) pour enlever le texte du document final, et il suffit de les décommenter (d'enlever le « % ») pour faire réapparaître le texte dans le document final ;
* de faire comme si l'on ne revenait pas à la ligne, en mettant un « % » à la fin de la ligne.
== Notes ==
<references />
== Voir aussi ==
=== Liens externes ===
* [http://www.ctan.org/tex-archive/info/symbols/comprehensive/symbols-a4.pdf The Comprehensive LaTeX Symbol List] (fichier PDF, 105 p, 3 Mo)
{{LaTeX-nav}}
[[Catégorie:LaTeX (livre)|Éléments de base]]
hwtl3qfcg79jytvuea3u8cqhkgud8rk
772928
772927
2026-09-23T08:17:55Z
Cdang
1202
/* Caractères réservés */ ponctuation
772928
wikitext
text/x-wiki
<noinclude>{{NavTitre|book={{BASEPAGENAME}}|prev=Lettre|next=Les classes}}</noinclude>
<noinclude>{{LaTeX/Navigation}}</noinclude>
== Bloc ==
Dans LaTeX, un bloc est une portion de texte comprise entre deux accolades « {…} ». Souvent, une instruction agit sur un bloc, par exemple :
<syntaxhighlight lang="latex">
\emph{texte}
</syntaxhighlight>
ou bien un bloc sert à limiter la zone sur laquelle agit une instruction de portée « infinie », par exemple
<syntaxhighlight lang="latex">
{\em texte}
</syntaxhighlight>
On utilisera parfois le bloc vide
<syntaxhighlight lang="latex">
{}
</syntaxhighlight>
: Les accolades n'apparaissent pas sur le rendu final
(voir la section suivante pour mettre des accolades dans le texte final).
== Caractères réservés ==
Dans le premier exemple, nous avons vu que la contre-oblique « \ » servait à indiquer les instructions, les accolades « {} » un bloc et que le tilde « ~ » indiquait l'espace insécable. Si nous voulons faire figurer ces caractères dans le document final, on ne peut donc pas les taper tels quels dans le fichier source. Il existe d'autres caractères réservés, en voici la liste :
{ } % # $ ^ ~ & _ \
l'avant-dernier caractère est le tiret de soulignement ou ''underscore'' (touche 8 sur un clavier de PC, ou touche à droite de la parenthèse fermante sur un clavier Mac).
Si nous voulons utiliser ces caractères, il faudra entrer :
* <code>\{</code> pour {
* <code>\}</code> pour }
* <code>\%</code> pour %
* <code>\#</code> pour #
* <code>\$</code> pour $
* <code>\textasciicircum</code> pour ^ (mnémotechnique anglais : ''text, ASCII, circumflex'') ;
* <code>\textasciitilde</code> pour ~ (mnémotechnique anglais : ''text, ASCII, tilde'') ;
* <code>\&</code> pour &
* <code>\_</code> pour _
* <code>\textbackslash</code> pour \ (mnémotechnique anglais : ''text, backslash'').
Les commandes avec un caractère réservé peuvent être suivies par n'importe quoi, il n'y a pas d'ambiguïté. Par contre, les commandes constituées de lettres ne peuvent pas être suivies de lettres : comment savoir où s'arrête la commande ? Elles peuvent par contre être suivies d'un signe autre qu'une lettre : un nombre, un signe de ponctuation…
Si l'on veut, dans le document final, accoler une lettre au caractère obtenu avec une telle commande, il faut indiquer la fin de la commande :
* soit laisser une espace après la commande ; cette espace sera ignorée pour le rendu final ;
* soit placer une paire d'accolades vides <code>{}</code>.
Par exemple
* <code>\textasciicircumA</code> génère une erreur ;
* <code>\textasciicircum˽A</code> et <code>\textasciicircum{}A</code> donnent « ^A » ;
* <code>\textasciicircum{}˽A</code> donne « ^ A ».
Le caractère « ˽ » sert à indiquer l'espace, pour clarifier les choses. On note ici que le fait que le fichier contienne un caractère espace (obtenu avec la barre d'espacement du clavier) ne signifie pas que le document final aura une espace<ref>En typographie, le mot espace est au féminin lorsqu'il désigne un écart horizontal</ref>.
== Diacritiques et ligatures ==
Au chapitre ''premier exemple'', nous avons vu qu'avec l'extension <code>inputenc</code>, nous pouvions choisir le jeu de caractères et donc utiliser les caractères accentués, les caractères de la langue française et de manière plus spécifique [[w:Diacritique|diacritiqués]], du clavier. Le problème se pose lorsque le clavier ne permet pas de faire les caractères que nous voulons, par exemple :
* les capitales accentuées<ref>en bonne typographie, on met les accents sur les capitales. Notez que certains systèmes (comme GNU/Linux) gèrent nativement les majuscules accentuées. Pour cela, activez le verrouillage majuscule (touche au-dessus du Shift) et appuyez sur la touche accentuée concernée.</ref> comme « É » ;
* les lettres des langues étrangères au clavier : accents français avec un clavier anglais, lettres danoises avec un clavier français, …
Voici quelques exemples :
* <code>\'e</code> ou <code>\'{e}</code> pour é ;
* <code>\`e</code> ou <code>\`{e}</code> pour è ;
* <code>\^\i</code> ou <code>\^{\i}</code> pour î ;
* <code>\`A</code> ou <code>\`{A}</code> pour À ;
* <code>\"e</code> ou <code>\"{e}</code> pour ë ;
* <code>\c˽c</code> ou <code>\c{c}</code> pour ç,
et ainsi de suite.
On remarque que :
* le diacritique est une commande qui est suivie directement par la lettre :
** <code>\'</code> (contre-oblique et apostrophe) pour l'accent aigu,
** <code>\`</code> (contre-oblique et apostrophe inversée) pour l'accent grave,
** <code>\^</code> (contre-oblique et chapeau) pour l'accent circonflexe,
** <code>\"</code> (contre-oblique et guillemet double anglais) pour le tréma,
** <code>\c</code> (contre-oblique et c) pour une cédille ; la lettre cédillée doit être mise entre accolades, ou bien séparée d'une espace ;
* pour mettre un accent sur le i minuscule, il faut utiliser <code>\i</code> (contre-oblique, i), afin de supprimer le point sur le i avant de placer l'accent.
On peut aussi utiliser les ligatures :
* <code>\oe</code> pour œ ;
* <code>\OE</code> pour Œ ;
* <code>\ae</code> pour æ ;
* <code>\AE</code> pour Æ.
Les physiciens pourront avoir recours à <code>\AA</code> pour Å.
Le problème des commandes composées de lettres (comme <code>\c</code>, <code>\i</code>, <code>\oe</code> et <code>\AA</code>) est le même que ci-dessus. Par exemple, il faut taper
* <code>c\oe˽ur</code> ou <code>c\oe{}ur</code> pour obtenir « cœur » ;
* <code>fa\^\i˽t</code> ou <code>fa\^\i{}t</code> pour obtenir « faît » ;
* <code>\c˽ca</code> ou <code>\c{c}a</code> pour obtenir « ça ».
Cette précaution est par contre inutile si le caractère suivant n'est pas une lettre. Comment alors indiquer que l'on veut une espace après ? Deux solutions :
* soit on indique explicitement la présence d'une espace dite « justifiante » (c'est-à-dire non insécable et de dimension variable pour s'ajuster à la longueur de la ligne et au nombre de caractères), avec la commande <code>\˽</code> (contre-oblique, espace) ;
* soit on indique la fin de la commande avec des accolades vides <code>{}</code>, le tout suivi d'une espace.
Par exemple :
<syntaxhighlight lang="tex">
On peut obtenir \OE\ avec \textbackslash OE, et \AE{} avec \textbackslash{}AE.
Le symbole de l'angstr\"om est \AA.
</syntaxhighlight>
donne
{{début rendu LaTeX}}
On peut obtenir Œ avec \OE, et Æ avec \AE. Le symbole de l'angström est Å.
{{fin rendu LaTeX}}
== Quelques autres caractères ==
Voici quelques autres caractères que l'on peut obtenir.
{| class="wikitable"
|+ Caractères LaTeX
|-
! scope="col" | Saisie
! scope="col" | Caractère
! scope="col" | Note
|-
| <code>\copyright</code> || © || ''{{lang|en|copyright}}''
|-
| <code>\dag</code> || † || obèle
|-
| <code>\ddag</code> || ‡ || double obèle
|-
| <code>\dots</code> || … || points de suspension (plus espacés que trois points)
|-
| <code>\o</code> || ø || o barré
|-
| <code>\O</code> || Ø ||notation de l'ensemble vide (mathématiques)
|-
| <code>\P</code> || ¶ || pied-de-mouche
|-
| <code>\pounds</code> || £ || livre sterling
|-
| <code>\S</code> || § || paragraphe
|-
| <code>\ss</code> || ß || eszett (lettre allemande)
|-
| <code>\textbar</code> || <nowiki>|</nowiki> || tube
|-
| <code>\textperiodcentered</code> || · || point centré
|-
| <code>\textregistered</code> || ® || ''{{lang|en|registred}}''
|-
| <code>\texttrademark</code> || ™ || ''{{lang|en|trade mark}}''
|-
| <code>\textvisiblespace</code> || ˽ || matérialisation d'une espace
|}
Certains caractères ont une saisie simplifiée.
{| class="wikitable"
|+ Caractères LaTeX
|-
! scope="col" | Saisie
! scope="col" | Caractère
! scope="col" | Note
|-
| <code>?`</code> || ¿ || « ` » est l'apostrophe inversée (accent grave) ; alternative : <code>\textquestiondown</code>
|-
| <code>!`</code> || ¡ || ''idem'' ; alternative : <code>\textexclamdown</code>
|-
| <code>-</code> || - || division (trait d'union)
|-
| <code>--</code> || – || tiret demi-cadratin, alternative : <code>\endash</code>
|-
| <code>---</code> || — || tiret cadratin, alternative : <code>\emdash</code>
|-
| <code id="guillemets1"><<</code> || « || guillemet français ouvrant, avec le codage de polices T1 mais [[#extensionfrancaise|voir ci-dessous]]
|-
| <code>>></code> || » || guillemet français fermant, avec le codage de polices T1 mais [[#extensionfrancaise|voir ci-dessous]]
|}
Les symboles ci-dessus peuvent aussi être mis directement dans le fichier <code>.tex</code> si vous utilisez un codage adéquat, comme l'ISO Latin 1 ou l'UTF8 (cf. ''[[../Premier exemple#Améliorations du code source|Premier exemple > Améliorations du code source]]''). Notez que l'utilisation des guillemets français tels quels ne provoque pas un affichage correct des espaces en français, dans le cas d'une citation, il faut donc utiliser <code>\og</code> et <code>\fg</code> à la place (voir ci-après).
L'euro (la monnaie) a été introduite récemment par rapport à l'histoire de LaTeX. Elle n'est donc pas disponible dans les fontes de base. Pour l'obtenir, il faut :
* charger l'extension <code>eurosym</code>, en mettant <code>\usepackage{eurosym}</code> dans le préambule ;
* dans le texte, utiliser la commande <code>\euro</code> (éventuellement <code>\euro{}</code>).
=== Cas du point d'abréviation anglais ===
En typographie anglaise, l'espace après un point final de phrase est plus grande que l'espace après un point d'abréviation. Par défaut, LaTeX considère qu'un point est un point final, mais il peut reconnaître dans certains cas les points d'abréviation (comme par exemple dans <code>U.F.O.</code>). Si l'on veut forcer un point d'abréviation, il faut utiliser <code>.\@</code> (point, contre-oblique, arobase).
; Exemple
<syntaxhighlight lang="tex">
U.F.O.\@ means ``undentified flying object''.
</syntaxhighlight>
Dans certains cas, il vaut mieux utiliser le tilde, espace insécable, qui permet d'avoir le bon espacement ''et'' de ne pas avoir de coupure en fin de ligne :
; Exemple
<syntaxhighlight lang="tex">
And here's to you Mrs.~Robinson
</syntaxhighlight>
À l'inverse, si l'on veut forcer un point de fin de ligne (par exemple phrase se terminant par une abréviation, le dernier point d'abréviation étant aussi point de fin de phrase), on peut écrire <code>\@.</code>, ou bien mettre le mot entre accolade.
; Exemple
<syntaxhighlight lang="tex">
This is a U.F.O\@.
</syntaxhighlight>
: ou bien
<syntaxhighlight lang="tex">
This is a {U.F.O.}
</syntaxhighlight>
Tout ceci ne s'applique pas en typographie française (l'espace est identique quel que soit le type de point), et l'extension <code>babel</code> ajuste automatiquement la taille de l'espace après un point (voir plus loin).
=== Autres langues ===
Voici quelques diacritiques (accents) utilisés dans d'autres langues que le français, telles que le roumain :
{|class="wikitable"
|+ Diacritiques en langues autres que le français
! scope="col" | Saisie
! scope="col" | Caractère
! scope="col" | Note
|-
| <code>\H{o}</code> || ő || double accent aigu : tréma long hongrois
|-
| <code>\~{n}</code> || ñ || tilde : espagnol, estonien, vietnamien, võro
|-
| <code>\k{a}</code> || ą || ogonek : polonais, lituanien
|-
| <code>\l{}</code> || ł || l barré : polonais)
|-
| <code>\={o}</code> || ō || macron : en français, accent « plat, neutre » (lorsque l'on hésite entre l'accent grave et l'aigu ou bien sur des capitales pour des raisons d'esthétique), transcription de l'arabe, du japonais (méthode Hepburn) ou du cantonnais (hanyu pinyin) ; letton, lituanien, latin (en typographie moderne, en remplacement de l'accent aigu)
|-
| <code>\b{o}</code> || <u>o</u> || trait souscrit ou soulignement : langues du Gabon, shoshone, transcription du dakelh
|-
| <code>\.{o}</code> || ȯ || point suscrit ou point en chef : écriture onciale (Moyen-Âge), vieil anglais, vieil irlandais, maltais, turc, polonais, lituanien, cheyenne, transcriptions du sanskrit, de l'arabe, du kazakh, du tadjik, du kurde, du livonien, du tchétchène, du venda, de l'igbo, de l'arménien, de l'ulithien et du basa ; en physique, indique la dérivation par rapport eu temps
|-
| <code>\d{u}</code> || ụ || point souscrit : asturien, vietnamien ''({{lang|vi|quốc ngữ}})'', allemand (dans les dictionnaires), transcription de langues indiennes ou afro-asiatiques, transcription de lettre incertaines en archéologie (épigraphie) ;
|-
| <code>\r{a}</code> || å || rond en chef, anneau en chef : suédois, danois, norvégien, tchèque, lituanien, wallon, picard, alphabet phonétique international (API)
|-
| <code>\u{o}</code> || ŏ || brève : alphabet cyrillique (biélorusse, russe, ukrainien, langues caucasiennes et turques, tchouvache), esperanto, roumain, turc, vietnamien ''({{lang|vi|quốc ngữ}})''
|-
| <code>\v{s}</code> || š || caron ou hatchek (háček), antiflexe, inflexe, circonflexe inversé ou chevron : slovène, tchèque, slovaque, croate, finnois, écriture latine du bosnien, du serbe et du tamazight, transcription du bulgare et du macédonien
|-
| <code>\t{oo}</code> || o͡o || tirant en chef, tirant suscrit, double brève, dos-d'âne, trait d’union papyrologique : alphabet phonétique international (API), alphabet phonétique ouralien (APO), transcription du cyrillique (méthode ALA-LC), notation Z (modélisation informatique), musique ''({{lang|it|legato}})''
|-
|<code>\cb{s}</code> || ș || La virgule souscrite peut être obtenue en utilisant <code>\cb{s}</code> du paquet <code>combelow</code> : roumain
|}
== <span id="extensionfrancaise">Extension française</span> ==
L'extension française utilisée fournit des commandes supplémentaires, facilitant l'édition.
Voici quelques exemples :
; Commandes communes à <code id="babel2">babel</code> option <code>french</code> et à <code>frenchle</code>
* <code id="guillemets2">\og</code> : « suivi d'une espace insécable ;
* <code>\fg</code> : » précédé d'une espace insécable ;
Si le caractère qui suit le guillemet fermant est un espace, il faut utiliser soit <code>\fg{} </code>, soit <code>\fg\˽</code>.
; Commandes de <code>babel</code> option <code>french</code>
* <code>\degres</code> : ° ;
* <code>\ier</code> : <sup>er</sup> ;
* <code>\iere</code> : <sup>re</sup> ;
* <code>\iers</code> : <sup>ers</sup> ;
* <code>\ieres</code> : <sup>res</sup> ;
* <code>\ieme</code> : <sup>e</sup> ;
* <code>\iemes</code> : <sup>es</sup> ;
* <code>\bsc{…}</code> : petites capitales, le mot n'étant pas césuré.
== Espaces et changements de ligne ==
On remarque dans l'exemple ci-dessus que le fait que l'on ait changé de ligne dans le fichier source ne provoque pas de changement de ligne dans le résultat final. Le changement de ligne simple équivaut à un espace. Par contre, si on laisse une ou plusieurs lignes vides, cela indique un changement de paragraphe, caractérisé par un retour à la ligne et :
* soit un alinéa rentrant (indentation), qui est le cas général en typographie française et anglaise ;
* soit par un interligne plus grand, solution souvent retenue pour des ouvrages techniques et popularisée par les navigateurs Web et le traitement de texte Microsoft Word, pour lesquels c'est la mise en page par défaut.
On peut laisser plusieurs lignes vides, cela équivaut à une seule ligne vide.
De même, si l'on met plusieurs espaces, c'est comme s'il n'y en avait qu'un seul.
Concernant les blancs entre les mots (espaces, au féminin), il existe plusieurs types d'espaces :
* espace justifiante (de dimension variable) : obtenu par une ou plusieurs pressions sur la barre d'espacement et/ou une pression sur la touche {{touche|Entrée}}, ou bien par <code>\˽</code> ;
* espace insécable : <code>~</code> (tilde) ;
* petite espace : <code>\,</code> ;
* espace fine : <code>\/</code> : espace encore plus petite, permettant de régler des problèmes de débordement de l'italique (<code>\emph{…\/}</code>), ou bien d'empêcher les ligatures.
Pour introduire un espace horizontal d'une longueur donnée, on utilise la commande <code>\hspace{''longueur''}</code> où ''longueur'' est un nombre avec une unité accolée :
* <code>mm</code> (millimètre) ;
* <code>cm</code> (centimètre) ;
* <code>pt</code> (point anglo-saxon) ;
* <code>dd</code> (point Didot) ;
* <code>ex</code> (hauteur d'x) ;
* <code>em</code> (cadratin).
Les unités <code>ex</code> et <code>em</code> sont proportionnelles au corps de la police :
* la hauteur d'x, parfois appelée à tort « hauteur d'œil », est la hauteur d'un bas de casse (lettre minuscule) sans hampe ni jambage, comme le ''x'' ;
* le cadratin est égal au corps de la police.
Par exemple :
: <code>\hspace{1cm}</code> pour un espace de un centimètre ;
: <code>\hspace{0,5em}</code> pour un espace d'un demi cadratin.
On peut utiliser un point ou une virgule comme séparateur décimal.
La commande <code>\hfill</code> introduit un espace « ressort » : il pousse ce qu'il y a à gauche et à droite pour occuper tout l'espace restant sur la ligne. S'il y a plusieurs commandes <code>\hfill</code> sur la même ligne, les espaces sont de même largeur.
De même, si l'on veut espacer des paragraphes, on utilise la commande <code>\vspace{''longueur''}</code>. On peut également utiliser les commandes :
* <code>\medskip</code> pour sauter une ligne « normale » ;
* <code>\smallskip</code> pour un « petit » saut de ligne ;
* <code>\bigskip</code> pour un « grand » saut de ligne.
La commande <code>\vfill</code> introduit un espace « ressort » : il pousse ce qu'il y a au dessus et en dessous pour occuper tout l'espace restant sur la page. S'il y a plusieurs commandes <code>\vfill</code> sur la même page, les espaces sont de même hauteur.
=== Structuration du code source ===
Comme pour tout code source de programme, on a intérêt à structurer le fichier LaTeX afin de retrouver facilement les passages que l'on veut.
LaTeX n'étant pas WYSIWYG, on peut organiser le texte comme l'on veut, ce qui facilite sa lecture à l'écran ; en particulier, on peut introduire un retour de ligne entre chaque portion de phrase, entre chaque proposition. Le fait de pouvoir sauter des lignes entre les paragraphes et mettre des espaces ou tabulations à profusion en début de ligne permet de donner une « forme » au code qui rend son analyse facile (notion d'indentation).
Par ailleurs, on peut introduire des commentaires en utilisant le signe <code>%</code>.
Notons que les commentaires permettent aussi :
* d'effacer une portion de texte sans la perdre : on met les lignes en commentaires (on place un « % » à leur début) pour enlever le texte du document final, et il suffit de les décommenter (d'enlever le « % ») pour faire réapparaître le texte dans le document final ;
* de faire comme si l'on ne revenait pas à la ligne, en mettant un « % » à la fin de la ligne.
== Notes ==
<references />
== Voir aussi ==
=== Liens externes ===
* [http://www.ctan.org/tex-archive/info/symbols/comprehensive/symbols-a4.pdf The Comprehensive LaTeX Symbol List] (fichier PDF, 105 p, 3 Mo)
{{LaTeX-nav}}
[[Catégorie:LaTeX (livre)|Éléments de base]]
rfs8o960e7mtrw2leiy1go8mvsosipi
772929
772928
2026-09-23T08:18:30Z
Cdang
1202
/* Diacritiques et ligatures */ majuscule
772929
wikitext
text/x-wiki
<noinclude>{{NavTitre|book={{BASEPAGENAME}}|prev=Lettre|next=Les classes}}</noinclude>
<noinclude>{{LaTeX/Navigation}}</noinclude>
== Bloc ==
Dans LaTeX, un bloc est une portion de texte comprise entre deux accolades « {…} ». Souvent, une instruction agit sur un bloc, par exemple :
<syntaxhighlight lang="latex">
\emph{texte}
</syntaxhighlight>
ou bien un bloc sert à limiter la zone sur laquelle agit une instruction de portée « infinie », par exemple
<syntaxhighlight lang="latex">
{\em texte}
</syntaxhighlight>
On utilisera parfois le bloc vide
<syntaxhighlight lang="latex">
{}
</syntaxhighlight>
: Les accolades n'apparaissent pas sur le rendu final
(voir la section suivante pour mettre des accolades dans le texte final).
== Caractères réservés ==
Dans le premier exemple, nous avons vu que la contre-oblique « \ » servait à indiquer les instructions, les accolades « {} » un bloc et que le tilde « ~ » indiquait l'espace insécable. Si nous voulons faire figurer ces caractères dans le document final, on ne peut donc pas les taper tels quels dans le fichier source. Il existe d'autres caractères réservés, en voici la liste :
{ } % # $ ^ ~ & _ \
l'avant-dernier caractère est le tiret de soulignement ou ''underscore'' (touche 8 sur un clavier de PC, ou touche à droite de la parenthèse fermante sur un clavier Mac).
Si nous voulons utiliser ces caractères, il faudra entrer :
* <code>\{</code> pour {
* <code>\}</code> pour }
* <code>\%</code> pour %
* <code>\#</code> pour #
* <code>\$</code> pour $
* <code>\textasciicircum</code> pour ^ (mnémotechnique anglais : ''text, ASCII, circumflex'') ;
* <code>\textasciitilde</code> pour ~ (mnémotechnique anglais : ''text, ASCII, tilde'') ;
* <code>\&</code> pour &
* <code>\_</code> pour _
* <code>\textbackslash</code> pour \ (mnémotechnique anglais : ''text, backslash'').
Les commandes avec un caractère réservé peuvent être suivies par n'importe quoi, il n'y a pas d'ambiguïté. Par contre, les commandes constituées de lettres ne peuvent pas être suivies de lettres : comment savoir où s'arrête la commande ? Elles peuvent par contre être suivies d'un signe autre qu'une lettre : un nombre, un signe de ponctuation…
Si l'on veut, dans le document final, accoler une lettre au caractère obtenu avec une telle commande, il faut indiquer la fin de la commande :
* soit laisser une espace après la commande ; cette espace sera ignorée pour le rendu final ;
* soit placer une paire d'accolades vides <code>{}</code>.
Par exemple
* <code>\textasciicircumA</code> génère une erreur ;
* <code>\textasciicircum˽A</code> et <code>\textasciicircum{}A</code> donnent « ^A » ;
* <code>\textasciicircum{}˽A</code> donne « ^ A ».
Le caractère « ˽ » sert à indiquer l'espace, pour clarifier les choses. On note ici que le fait que le fichier contienne un caractère espace (obtenu avec la barre d'espacement du clavier) ne signifie pas que le document final aura une espace<ref>En typographie, le mot espace est au féminin lorsqu'il désigne un écart horizontal</ref>.
== Diacritiques et ligatures ==
Au chapitre ''premier exemple'', nous avons vu qu'avec l'extension <code>inputenc</code>, nous pouvions choisir le jeu de caractères et donc utiliser les caractères accentués, les caractères de la langue française et de manière plus spécifique [[w:Diacritique|diacritiqués]], du clavier. Le problème se pose lorsque le clavier ne permet pas de faire les caractères que nous voulons, par exemple :
* les capitales accentuées<ref>En bonne typographie, on met les accents sur les capitales. Notez que certains systèmes (comme GNU/Linux) gèrent nativement les majuscules accentuées. Pour cela, activez le verrouillage majuscule (touche au-dessus du Shift) et appuyez sur la touche accentuée concernée.</ref> comme « É » ;
* les lettres des langues étrangères au clavier : accents français avec un clavier anglais, lettres danoises avec un clavier français, …
Voici quelques exemples :
* <code>\'e</code> ou <code>\'{e}</code> pour é ;
* <code>\`e</code> ou <code>\`{e}</code> pour è ;
* <code>\^\i</code> ou <code>\^{\i}</code> pour î ;
* <code>\`A</code> ou <code>\`{A}</code> pour À ;
* <code>\"e</code> ou <code>\"{e}</code> pour ë ;
* <code>\c˽c</code> ou <code>\c{c}</code> pour ç,
et ainsi de suite.
On remarque que :
* le diacritique est une commande qui est suivie directement par la lettre :
** <code>\'</code> (contre-oblique et apostrophe) pour l'accent aigu,
** <code>\`</code> (contre-oblique et apostrophe inversée) pour l'accent grave,
** <code>\^</code> (contre-oblique et chapeau) pour l'accent circonflexe,
** <code>\"</code> (contre-oblique et guillemet double anglais) pour le tréma,
** <code>\c</code> (contre-oblique et c) pour une cédille ; la lettre cédillée doit être mise entre accolades, ou bien séparée d'une espace ;
* pour mettre un accent sur le i minuscule, il faut utiliser <code>\i</code> (contre-oblique, i), afin de supprimer le point sur le i avant de placer l'accent.
On peut aussi utiliser les ligatures :
* <code>\oe</code> pour œ ;
* <code>\OE</code> pour Œ ;
* <code>\ae</code> pour æ ;
* <code>\AE</code> pour Æ.
Les physiciens pourront avoir recours à <code>\AA</code> pour Å.
Le problème des commandes composées de lettres (comme <code>\c</code>, <code>\i</code>, <code>\oe</code> et <code>\AA</code>) est le même que ci-dessus. Par exemple, il faut taper
* <code>c\oe˽ur</code> ou <code>c\oe{}ur</code> pour obtenir « cœur » ;
* <code>fa\^\i˽t</code> ou <code>fa\^\i{}t</code> pour obtenir « faît » ;
* <code>\c˽ca</code> ou <code>\c{c}a</code> pour obtenir « ça ».
Cette précaution est par contre inutile si le caractère suivant n'est pas une lettre. Comment alors indiquer que l'on veut une espace après ? Deux solutions :
* soit on indique explicitement la présence d'une espace dite « justifiante » (c'est-à-dire non insécable et de dimension variable pour s'ajuster à la longueur de la ligne et au nombre de caractères), avec la commande <code>\˽</code> (contre-oblique, espace) ;
* soit on indique la fin de la commande avec des accolades vides <code>{}</code>, le tout suivi d'une espace.
Par exemple :
<syntaxhighlight lang="tex">
On peut obtenir \OE\ avec \textbackslash OE, et \AE{} avec \textbackslash{}AE.
Le symbole de l'angstr\"om est \AA.
</syntaxhighlight>
donne
{{début rendu LaTeX}}
On peut obtenir Œ avec \OE, et Æ avec \AE. Le symbole de l'angström est Å.
{{fin rendu LaTeX}}
== Quelques autres caractères ==
Voici quelques autres caractères que l'on peut obtenir.
{| class="wikitable"
|+ Caractères LaTeX
|-
! scope="col" | Saisie
! scope="col" | Caractère
! scope="col" | Note
|-
| <code>\copyright</code> || © || ''{{lang|en|copyright}}''
|-
| <code>\dag</code> || † || obèle
|-
| <code>\ddag</code> || ‡ || double obèle
|-
| <code>\dots</code> || … || points de suspension (plus espacés que trois points)
|-
| <code>\o</code> || ø || o barré
|-
| <code>\O</code> || Ø ||notation de l'ensemble vide (mathématiques)
|-
| <code>\P</code> || ¶ || pied-de-mouche
|-
| <code>\pounds</code> || £ || livre sterling
|-
| <code>\S</code> || § || paragraphe
|-
| <code>\ss</code> || ß || eszett (lettre allemande)
|-
| <code>\textbar</code> || <nowiki>|</nowiki> || tube
|-
| <code>\textperiodcentered</code> || · || point centré
|-
| <code>\textregistered</code> || ® || ''{{lang|en|registred}}''
|-
| <code>\texttrademark</code> || ™ || ''{{lang|en|trade mark}}''
|-
| <code>\textvisiblespace</code> || ˽ || matérialisation d'une espace
|}
Certains caractères ont une saisie simplifiée.
{| class="wikitable"
|+ Caractères LaTeX
|-
! scope="col" | Saisie
! scope="col" | Caractère
! scope="col" | Note
|-
| <code>?`</code> || ¿ || « ` » est l'apostrophe inversée (accent grave) ; alternative : <code>\textquestiondown</code>
|-
| <code>!`</code> || ¡ || ''idem'' ; alternative : <code>\textexclamdown</code>
|-
| <code>-</code> || - || division (trait d'union)
|-
| <code>--</code> || – || tiret demi-cadratin, alternative : <code>\endash</code>
|-
| <code>---</code> || — || tiret cadratin, alternative : <code>\emdash</code>
|-
| <code id="guillemets1"><<</code> || « || guillemet français ouvrant, avec le codage de polices T1 mais [[#extensionfrancaise|voir ci-dessous]]
|-
| <code>>></code> || » || guillemet français fermant, avec le codage de polices T1 mais [[#extensionfrancaise|voir ci-dessous]]
|}
Les symboles ci-dessus peuvent aussi être mis directement dans le fichier <code>.tex</code> si vous utilisez un codage adéquat, comme l'ISO Latin 1 ou l'UTF8 (cf. ''[[../Premier exemple#Améliorations du code source|Premier exemple > Améliorations du code source]]''). Notez que l'utilisation des guillemets français tels quels ne provoque pas un affichage correct des espaces en français, dans le cas d'une citation, il faut donc utiliser <code>\og</code> et <code>\fg</code> à la place (voir ci-après).
L'euro (la monnaie) a été introduite récemment par rapport à l'histoire de LaTeX. Elle n'est donc pas disponible dans les fontes de base. Pour l'obtenir, il faut :
* charger l'extension <code>eurosym</code>, en mettant <code>\usepackage{eurosym}</code> dans le préambule ;
* dans le texte, utiliser la commande <code>\euro</code> (éventuellement <code>\euro{}</code>).
=== Cas du point d'abréviation anglais ===
En typographie anglaise, l'espace après un point final de phrase est plus grande que l'espace après un point d'abréviation. Par défaut, LaTeX considère qu'un point est un point final, mais il peut reconnaître dans certains cas les points d'abréviation (comme par exemple dans <code>U.F.O.</code>). Si l'on veut forcer un point d'abréviation, il faut utiliser <code>.\@</code> (point, contre-oblique, arobase).
; Exemple
<syntaxhighlight lang="tex">
U.F.O.\@ means ``undentified flying object''.
</syntaxhighlight>
Dans certains cas, il vaut mieux utiliser le tilde, espace insécable, qui permet d'avoir le bon espacement ''et'' de ne pas avoir de coupure en fin de ligne :
; Exemple
<syntaxhighlight lang="tex">
And here's to you Mrs.~Robinson
</syntaxhighlight>
À l'inverse, si l'on veut forcer un point de fin de ligne (par exemple phrase se terminant par une abréviation, le dernier point d'abréviation étant aussi point de fin de phrase), on peut écrire <code>\@.</code>, ou bien mettre le mot entre accolade.
; Exemple
<syntaxhighlight lang="tex">
This is a U.F.O\@.
</syntaxhighlight>
: ou bien
<syntaxhighlight lang="tex">
This is a {U.F.O.}
</syntaxhighlight>
Tout ceci ne s'applique pas en typographie française (l'espace est identique quel que soit le type de point), et l'extension <code>babel</code> ajuste automatiquement la taille de l'espace après un point (voir plus loin).
=== Autres langues ===
Voici quelques diacritiques (accents) utilisés dans d'autres langues que le français, telles que le roumain :
{|class="wikitable"
|+ Diacritiques en langues autres que le français
! scope="col" | Saisie
! scope="col" | Caractère
! scope="col" | Note
|-
| <code>\H{o}</code> || ő || double accent aigu : tréma long hongrois
|-
| <code>\~{n}</code> || ñ || tilde : espagnol, estonien, vietnamien, võro
|-
| <code>\k{a}</code> || ą || ogonek : polonais, lituanien
|-
| <code>\l{}</code> || ł || l barré : polonais)
|-
| <code>\={o}</code> || ō || macron : en français, accent « plat, neutre » (lorsque l'on hésite entre l'accent grave et l'aigu ou bien sur des capitales pour des raisons d'esthétique), transcription de l'arabe, du japonais (méthode Hepburn) ou du cantonnais (hanyu pinyin) ; letton, lituanien, latin (en typographie moderne, en remplacement de l'accent aigu)
|-
| <code>\b{o}</code> || <u>o</u> || trait souscrit ou soulignement : langues du Gabon, shoshone, transcription du dakelh
|-
| <code>\.{o}</code> || ȯ || point suscrit ou point en chef : écriture onciale (Moyen-Âge), vieil anglais, vieil irlandais, maltais, turc, polonais, lituanien, cheyenne, transcriptions du sanskrit, de l'arabe, du kazakh, du tadjik, du kurde, du livonien, du tchétchène, du venda, de l'igbo, de l'arménien, de l'ulithien et du basa ; en physique, indique la dérivation par rapport eu temps
|-
| <code>\d{u}</code> || ụ || point souscrit : asturien, vietnamien ''({{lang|vi|quốc ngữ}})'', allemand (dans les dictionnaires), transcription de langues indiennes ou afro-asiatiques, transcription de lettre incertaines en archéologie (épigraphie) ;
|-
| <code>\r{a}</code> || å || rond en chef, anneau en chef : suédois, danois, norvégien, tchèque, lituanien, wallon, picard, alphabet phonétique international (API)
|-
| <code>\u{o}</code> || ŏ || brève : alphabet cyrillique (biélorusse, russe, ukrainien, langues caucasiennes et turques, tchouvache), esperanto, roumain, turc, vietnamien ''({{lang|vi|quốc ngữ}})''
|-
| <code>\v{s}</code> || š || caron ou hatchek (háček), antiflexe, inflexe, circonflexe inversé ou chevron : slovène, tchèque, slovaque, croate, finnois, écriture latine du bosnien, du serbe et du tamazight, transcription du bulgare et du macédonien
|-
| <code>\t{oo}</code> || o͡o || tirant en chef, tirant suscrit, double brève, dos-d'âne, trait d’union papyrologique : alphabet phonétique international (API), alphabet phonétique ouralien (APO), transcription du cyrillique (méthode ALA-LC), notation Z (modélisation informatique), musique ''({{lang|it|legato}})''
|-
|<code>\cb{s}</code> || ș || La virgule souscrite peut être obtenue en utilisant <code>\cb{s}</code> du paquet <code>combelow</code> : roumain
|}
== <span id="extensionfrancaise">Extension française</span> ==
L'extension française utilisée fournit des commandes supplémentaires, facilitant l'édition.
Voici quelques exemples :
; Commandes communes à <code id="babel2">babel</code> option <code>french</code> et à <code>frenchle</code>
* <code id="guillemets2">\og</code> : « suivi d'une espace insécable ;
* <code>\fg</code> : » précédé d'une espace insécable ;
Si le caractère qui suit le guillemet fermant est un espace, il faut utiliser soit <code>\fg{} </code>, soit <code>\fg\˽</code>.
; Commandes de <code>babel</code> option <code>french</code>
* <code>\degres</code> : ° ;
* <code>\ier</code> : <sup>er</sup> ;
* <code>\iere</code> : <sup>re</sup> ;
* <code>\iers</code> : <sup>ers</sup> ;
* <code>\ieres</code> : <sup>res</sup> ;
* <code>\ieme</code> : <sup>e</sup> ;
* <code>\iemes</code> : <sup>es</sup> ;
* <code>\bsc{…}</code> : petites capitales, le mot n'étant pas césuré.
== Espaces et changements de ligne ==
On remarque dans l'exemple ci-dessus que le fait que l'on ait changé de ligne dans le fichier source ne provoque pas de changement de ligne dans le résultat final. Le changement de ligne simple équivaut à un espace. Par contre, si on laisse une ou plusieurs lignes vides, cela indique un changement de paragraphe, caractérisé par un retour à la ligne et :
* soit un alinéa rentrant (indentation), qui est le cas général en typographie française et anglaise ;
* soit par un interligne plus grand, solution souvent retenue pour des ouvrages techniques et popularisée par les navigateurs Web et le traitement de texte Microsoft Word, pour lesquels c'est la mise en page par défaut.
On peut laisser plusieurs lignes vides, cela équivaut à une seule ligne vide.
De même, si l'on met plusieurs espaces, c'est comme s'il n'y en avait qu'un seul.
Concernant les blancs entre les mots (espaces, au féminin), il existe plusieurs types d'espaces :
* espace justifiante (de dimension variable) : obtenu par une ou plusieurs pressions sur la barre d'espacement et/ou une pression sur la touche {{touche|Entrée}}, ou bien par <code>\˽</code> ;
* espace insécable : <code>~</code> (tilde) ;
* petite espace : <code>\,</code> ;
* espace fine : <code>\/</code> : espace encore plus petite, permettant de régler des problèmes de débordement de l'italique (<code>\emph{…\/}</code>), ou bien d'empêcher les ligatures.
Pour introduire un espace horizontal d'une longueur donnée, on utilise la commande <code>\hspace{''longueur''}</code> où ''longueur'' est un nombre avec une unité accolée :
* <code>mm</code> (millimètre) ;
* <code>cm</code> (centimètre) ;
* <code>pt</code> (point anglo-saxon) ;
* <code>dd</code> (point Didot) ;
* <code>ex</code> (hauteur d'x) ;
* <code>em</code> (cadratin).
Les unités <code>ex</code> et <code>em</code> sont proportionnelles au corps de la police :
* la hauteur d'x, parfois appelée à tort « hauteur d'œil », est la hauteur d'un bas de casse (lettre minuscule) sans hampe ni jambage, comme le ''x'' ;
* le cadratin est égal au corps de la police.
Par exemple :
: <code>\hspace{1cm}</code> pour un espace de un centimètre ;
: <code>\hspace{0,5em}</code> pour un espace d'un demi cadratin.
On peut utiliser un point ou une virgule comme séparateur décimal.
La commande <code>\hfill</code> introduit un espace « ressort » : il pousse ce qu'il y a à gauche et à droite pour occuper tout l'espace restant sur la ligne. S'il y a plusieurs commandes <code>\hfill</code> sur la même ligne, les espaces sont de même largeur.
De même, si l'on veut espacer des paragraphes, on utilise la commande <code>\vspace{''longueur''}</code>. On peut également utiliser les commandes :
* <code>\medskip</code> pour sauter une ligne « normale » ;
* <code>\smallskip</code> pour un « petit » saut de ligne ;
* <code>\bigskip</code> pour un « grand » saut de ligne.
La commande <code>\vfill</code> introduit un espace « ressort » : il pousse ce qu'il y a au dessus et en dessous pour occuper tout l'espace restant sur la page. S'il y a plusieurs commandes <code>\vfill</code> sur la même page, les espaces sont de même hauteur.
=== Structuration du code source ===
Comme pour tout code source de programme, on a intérêt à structurer le fichier LaTeX afin de retrouver facilement les passages que l'on veut.
LaTeX n'étant pas WYSIWYG, on peut organiser le texte comme l'on veut, ce qui facilite sa lecture à l'écran ; en particulier, on peut introduire un retour de ligne entre chaque portion de phrase, entre chaque proposition. Le fait de pouvoir sauter des lignes entre les paragraphes et mettre des espaces ou tabulations à profusion en début de ligne permet de donner une « forme » au code qui rend son analyse facile (notion d'indentation).
Par ailleurs, on peut introduire des commentaires en utilisant le signe <code>%</code>.
Notons que les commentaires permettent aussi :
* d'effacer une portion de texte sans la perdre : on met les lignes en commentaires (on place un « % » à leur début) pour enlever le texte du document final, et il suffit de les décommenter (d'enlever le « % ») pour faire réapparaître le texte dans le document final ;
* de faire comme si l'on ne revenait pas à la ligne, en mettant un « % » à la fin de la ligne.
== Notes ==
<references />
== Voir aussi ==
=== Liens externes ===
* [http://www.ctan.org/tex-archive/info/symbols/comprehensive/symbols-a4.pdf The Comprehensive LaTeX Symbol List] (fichier PDF, 105 p, 3 Mo)
{{LaTeX-nav}}
[[Catégorie:LaTeX (livre)|Éléments de base]]
cp9rxaj9t79a7ehk1au6tszinu8r14n
Fonctionnement d'un ordinateur/Les processeurs superscalaires
0
65956
772815
772807
2026-09-22T13:53:03Z
Mewtow
31375
/* Les contraintes d’appariement */
772815
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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 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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
==Les contraintes d’appariement==
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les '''CPU superscalaires historiques''' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Les branchements sont traités comme des opérations entières, dans l'ALU entière. La seule différence est que leur résultat est envoyé au séquenceur pour que celui-ci altére le ''program counter''.
Pour les accès mémoire, c'est la même chose : µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Branchement ||Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Opération entière ||Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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|Émission 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. Avec la seconde, les branchements sont traités comme des instructions entières. Le premier cas est illustré ci-dessous.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
* Unité mémoire
| FPU
| Unité de branchement
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
* µops mémoire.
| Opération flottante
| Branchements
|}
La première solution impose la présence d'une unité de branchement séparée, avec son port d'émission. La seconde peut exécuter les branchements soit dans une ALU entière, soit dans une unité de branchement reliée au port d'émission pour les opérations entières. Elle permet d'économiser beaucoup de transistors dans l'unité d'émission/chargement, car on émet une µops en moins. De plus, utiliser l'ALU entière permet d'économiser une unité de calcul.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==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. Les unités de calcul sont aussi dupliquées, sauf sur les tout premiers CPU superscalaires historiques, qu'on abordera dans ce qui suit. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 unités de calcul d'un processeur superscalaire===
Le nombre d'unités de calcul dépend fortement des contraintes d’appariement du processeur.
Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif !
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, qui 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 ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée. Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
Implémenter un CPU superscalaire avec une ALU entière, une FPU et une unité mémoire ; est assez simple et on verra comment faire dans la suite. Mais il faut avouer que cela ne donne pas des CPU superscalaires efficaces. En pratique, les CPU superscalaires sont un intermédiaire entre ces deux cas extrêmes : certaines unités de calcul sont dupliquées en plusieurs exemplaires, mais pas toutes. La tendance générale est de dupliquer les unités de calcul entière, le reste n'étant tout simplement pas fait.
Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium. C'était un CPU double émission qui n'implémentait pas la double émission entière-flottante. A la place, il avait deux ALU entières, un multiplieur, une FPU et une unité mémoire. Il pouvait exécuter les combinaisons INT + INT, INT + MUL, et INT + MEM. Par contre, il ne pouvait pas émettre une instruction flottante avec une autre. En clair, rien à voir avec une sorte d'évolution des designs précédents. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
Typiquement, les ALU entières simples sont dupliquées, mais pas le circuit multiplieur, ni le ''barrel shifter''. De moins, sauf sur les processeurs superscalaires très larges. 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. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir exécuter deux multiplications à la fois, idem pour les décalages.
La FPU aussi n'est pas multipliée, ou du moins pas à l'identique. Je fais cette précision car une petite optimisation permet d'émettre deux opérations flottantes sans dupliquer la FPU. La '''double émission FADD/FMUL''' utilise 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. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants, mais ils sont souvent sur le port avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour les unités mémoire, nous mettons cela de côté pour le moment. Mais en attendant, nous allons supposer qu'elle n'est pas dupliquée dans les sections qui vont suivre.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire'''. 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.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
6ee8qg1v1ts2ha419kqe65p8q6betvq
772816
772815
2026-09-22T13:57:34Z
Mewtow
31375
772816
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les '''CPU superscalaires historiques''' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Les branchements sont traités comme des opérations entières, dans l'ALU entière. La seule différence est que leur résultat est envoyé au séquenceur pour que celui-ci altére le ''program counter''.
Pour les accès mémoire, c'est la même chose : µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Branchement ||Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Opération entière ||Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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|Émission 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. Avec la seconde, les branchements sont traités comme des instructions entières. Le premier cas est illustré ci-dessous.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
* Unité mémoire
| FPU
| Unité de branchement
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
* µops mémoire.
| Opération flottante
| Branchements
|}
La première solution impose la présence d'une unité de branchement séparée, avec son port d'émission. La seconde peut exécuter les branchements soit dans une ALU entière, soit dans une unité de branchement reliée au port d'émission pour les opérations entières. Elle permet d'économiser beaucoup de transistors dans l'unité d'émission/chargement, car on émet une µops en moins. De plus, utiliser l'ALU entière permet d'économiser une unité de calcul.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==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. Les unités de calcul sont aussi dupliquées, sauf sur les tout premiers CPU superscalaires historiques, qu'on abordera dans ce qui suit. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 unités de calcul d'un processeur superscalaire===
Le nombre d'unités de calcul dépend fortement des contraintes d’appariement du processeur.
Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif !
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, qui 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 ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée. Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
Implémenter un CPU superscalaire avec une ALU entière, une FPU et une unité mémoire ; est assez simple et on verra comment faire dans la suite. Mais il faut avouer que cela ne donne pas des CPU superscalaires efficaces. En pratique, les CPU superscalaires sont un intermédiaire entre ces deux cas extrêmes : certaines unités de calcul sont dupliquées en plusieurs exemplaires, mais pas toutes. La tendance générale est de dupliquer les unités de calcul entière, le reste n'étant tout simplement pas fait.
Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium. C'était un CPU double émission qui n'implémentait pas la double émission entière-flottante. A la place, il avait deux ALU entières, un multiplieur, une FPU et une unité mémoire. Il pouvait exécuter les combinaisons INT + INT, INT + MUL, et INT + MEM. Par contre, il ne pouvait pas émettre une instruction flottante avec une autre. En clair, rien à voir avec une sorte d'évolution des designs précédents. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
Typiquement, les ALU entières simples sont dupliquées, mais pas le circuit multiplieur, ni le ''barrel shifter''. De moins, sauf sur les processeurs superscalaires très larges. 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. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir exécuter deux multiplications à la fois, idem pour les décalages.
La FPU aussi n'est pas multipliée, ou du moins pas à l'identique. Je fais cette précision car une petite optimisation permet d'émettre deux opérations flottantes sans dupliquer la FPU. La '''double émission FADD/FMUL''' utilise 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. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants, mais ils sont souvent sur le port avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour les unités mémoire, nous mettons cela de côté pour le moment. Mais en attendant, nous allons supposer qu'elle n'est pas dupliquée dans les sections qui vont suivre.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire'''. 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.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
6020g988ik98pwzx69t5w3ca0c3tzi8
772817
772816
2026-09-22T14:00:26Z
Mewtow
31375
/* Le nombre de voies d'un processeur superscalaire */
772817
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant *toute chose égale par ailleurs*. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les '''CPU superscalaires historiques''' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Les branchements sont traités comme des opérations entières, dans l'ALU entière. La seule différence est que leur résultat est envoyé au séquenceur pour que celui-ci altére le ''program counter''.
Pour les accès mémoire, c'est la même chose : µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Branchement ||Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Opération entière ||Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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|Émission 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. Avec la seconde, les branchements sont traités comme des instructions entières. Le premier cas est illustré ci-dessous.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
* Unité mémoire
| FPU
| Unité de branchement
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
* µops mémoire.
| Opération flottante
| Branchements
|}
La première solution impose la présence d'une unité de branchement séparée, avec son port d'émission. La seconde peut exécuter les branchements soit dans une ALU entière, soit dans une unité de branchement reliée au port d'émission pour les opérations entières. Elle permet d'économiser beaucoup de transistors dans l'unité d'émission/chargement, car on émet une µops en moins. De plus, utiliser l'ALU entière permet d'économiser une unité de calcul.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==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. Les unités de calcul sont aussi dupliquées, sauf sur les tout premiers CPU superscalaires historiques, qu'on abordera dans ce qui suit. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 unités de calcul d'un processeur superscalaire===
Le nombre d'unités de calcul dépend fortement des contraintes d’appariement du processeur.
Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif !
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, qui 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 ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée. Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
Implémenter un CPU superscalaire avec une ALU entière, une FPU et une unité mémoire ; est assez simple et on verra comment faire dans la suite. Mais il faut avouer que cela ne donne pas des CPU superscalaires efficaces. En pratique, les CPU superscalaires sont un intermédiaire entre ces deux cas extrêmes : certaines unités de calcul sont dupliquées en plusieurs exemplaires, mais pas toutes. La tendance générale est de dupliquer les unités de calcul entière, le reste n'étant tout simplement pas fait.
Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium. C'était un CPU double émission qui n'implémentait pas la double émission entière-flottante. A la place, il avait deux ALU entières, un multiplieur, une FPU et une unité mémoire. Il pouvait exécuter les combinaisons INT + INT, INT + MUL, et INT + MEM. Par contre, il ne pouvait pas émettre une instruction flottante avec une autre. En clair, rien à voir avec une sorte d'évolution des designs précédents. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
Typiquement, les ALU entières simples sont dupliquées, mais pas le circuit multiplieur, ni le ''barrel shifter''. De moins, sauf sur les processeurs superscalaires très larges. 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. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir exécuter deux multiplications à la fois, idem pour les décalages.
La FPU aussi n'est pas multipliée, ou du moins pas à l'identique. Je fais cette précision car une petite optimisation permet d'émettre deux opérations flottantes sans dupliquer la FPU. La '''double émission FADD/FMUL''' utilise 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. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants, mais ils sont souvent sur le port avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour les unités mémoire, nous mettons cela de côté pour le moment. Mais en attendant, nous allons supposer qu'elle n'est pas dupliquée dans les sections qui vont suivre.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire'''. 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.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
o7ysvzit4ztxrumyaj6tetrlb1mdjuf
772818
772817
2026-09-22T14:01:01Z
Mewtow
31375
/* L'évolution historique des processeurs superscalaires */
772818
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant *toute chose égale par ailleurs*. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Les branchements sont traités comme des opérations entières, dans l'ALU entière. La seule différence est que leur résultat est envoyé au séquenceur pour que celui-ci altére le ''program counter''.
Pour les accès mémoire, c'est la même chose : µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Branchement ||Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Opération entière ||Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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|Émission 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. Avec la seconde, les branchements sont traités comme des instructions entières. Le premier cas est illustré ci-dessous.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
* Unité mémoire
| FPU
| Unité de branchement
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
* µops mémoire.
| Opération flottante
| Branchements
|}
La première solution impose la présence d'une unité de branchement séparée, avec son port d'émission. La seconde peut exécuter les branchements soit dans une ALU entière, soit dans une unité de branchement reliée au port d'émission pour les opérations entières. Elle permet d'économiser beaucoup de transistors dans l'unité d'émission/chargement, car on émet une µops en moins. De plus, utiliser l'ALU entière permet d'économiser une unité de calcul.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==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. Les unités de calcul sont aussi dupliquées, sauf sur les tout premiers CPU superscalaires historiques, qu'on abordera dans ce qui suit. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 unités de calcul d'un processeur superscalaire===
Le nombre d'unités de calcul dépend fortement des contraintes d’appariement du processeur.
Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif !
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, qui 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 ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée. Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
Implémenter un CPU superscalaire avec une ALU entière, une FPU et une unité mémoire ; est assez simple et on verra comment faire dans la suite. Mais il faut avouer que cela ne donne pas des CPU superscalaires efficaces. En pratique, les CPU superscalaires sont un intermédiaire entre ces deux cas extrêmes : certaines unités de calcul sont dupliquées en plusieurs exemplaires, mais pas toutes. La tendance générale est de dupliquer les unités de calcul entière, le reste n'étant tout simplement pas fait.
Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium. C'était un CPU double émission qui n'implémentait pas la double émission entière-flottante. A la place, il avait deux ALU entières, un multiplieur, une FPU et une unité mémoire. Il pouvait exécuter les combinaisons INT + INT, INT + MUL, et INT + MEM. Par contre, il ne pouvait pas émettre une instruction flottante avec une autre. En clair, rien à voir avec une sorte d'évolution des designs précédents. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
Typiquement, les ALU entières simples sont dupliquées, mais pas le circuit multiplieur, ni le ''barrel shifter''. De moins, sauf sur les processeurs superscalaires très larges. 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. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir exécuter deux multiplications à la fois, idem pour les décalages.
La FPU aussi n'est pas multipliée, ou du moins pas à l'identique. Je fais cette précision car une petite optimisation permet d'émettre deux opérations flottantes sans dupliquer la FPU. La '''double émission FADD/FMUL''' utilise 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. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants, mais ils sont souvent sur le port avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour les unités mémoire, nous mettons cela de côté pour le moment. Mais en attendant, nous allons supposer qu'elle n'est pas dupliquée dans les sections qui vont suivre.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire'''. 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.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
1d8opmov5mtkvk7zon066usj49lnbxo
772821
772818
2026-09-22T14:07:03Z
Mewtow
31375
/* La double émission entière-flottante */
772821
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant *toute chose égale par ailleurs*. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ?
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Branchement ||Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Opération entière ||Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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|Émission 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. Avec la seconde, les branchements sont traités comme des instructions entières. Le premier cas est illustré ci-dessous.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
* Unité mémoire
| FPU
| Unité de branchement
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
* µops mémoire.
| Opération flottante
| Branchements
|}
La première solution impose la présence d'une unité de branchement séparée, avec son port d'émission. La seconde peut exécuter les branchements soit dans une ALU entière, soit dans une unité de branchement reliée au port d'émission pour les opérations entières. Elle permet d'économiser beaucoup de transistors dans l'unité d'émission/chargement, car on émet une µops en moins. De plus, utiliser l'ALU entière permet d'économiser une unité de calcul.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==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. Les unités de calcul sont aussi dupliquées, sauf sur les tout premiers CPU superscalaires historiques, qu'on abordera dans ce qui suit. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 unités de calcul d'un processeur superscalaire===
Le nombre d'unités de calcul dépend fortement des contraintes d’appariement du processeur.
Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif !
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, qui 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 ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée. Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
Implémenter un CPU superscalaire avec une ALU entière, une FPU et une unité mémoire ; est assez simple et on verra comment faire dans la suite. Mais il faut avouer que cela ne donne pas des CPU superscalaires efficaces. En pratique, les CPU superscalaires sont un intermédiaire entre ces deux cas extrêmes : certaines unités de calcul sont dupliquées en plusieurs exemplaires, mais pas toutes. La tendance générale est de dupliquer les unités de calcul entière, le reste n'étant tout simplement pas fait.
Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium. C'était un CPU double émission qui n'implémentait pas la double émission entière-flottante. A la place, il avait deux ALU entières, un multiplieur, une FPU et une unité mémoire. Il pouvait exécuter les combinaisons INT + INT, INT + MUL, et INT + MEM. Par contre, il ne pouvait pas émettre une instruction flottante avec une autre. En clair, rien à voir avec une sorte d'évolution des designs précédents. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
Typiquement, les ALU entières simples sont dupliquées, mais pas le circuit multiplieur, ni le ''barrel shifter''. De moins, sauf sur les processeurs superscalaires très larges. 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. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir exécuter deux multiplications à la fois, idem pour les décalages.
La FPU aussi n'est pas multipliée, ou du moins pas à l'identique. Je fais cette précision car une petite optimisation permet d'émettre deux opérations flottantes sans dupliquer la FPU. La '''double émission FADD/FMUL''' utilise 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. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants, mais ils sont souvent sur le port avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour les unités mémoire, nous mettons cela de côté pour le moment. Mais en attendant, nous allons supposer qu'elle n'est pas dupliquée dans les sections qui vont suivre.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire'''. 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.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
9o9rtqeo9wo045d6xhxuesitgqzpwbt
772823
772821
2026-09-22T14:13:51Z
Mewtow
31375
/* L'émission parallèle des branchements */
772823
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant *toute chose égale par ailleurs*. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ?
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Branchement ||Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Opération entière ||Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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|Émission 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. Avec la seconde, les branchements sont traités comme des instructions entières. Le premier cas est illustré ci-dessous.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
* Unité mémoire
| FPU
| Unité de branchement
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
* µops mémoire.
| Opération flottante
| Branchements
|}
La première solution impose la présence d'une unité de branchement séparée, avec son port d'émission. La seconde peut exécuter les branchements soit dans une ALU entière, soit dans une unité de branchement reliée au port d'émission pour les opérations entières. Elle permet d'économiser beaucoup de transistors dans l'unité d'émission/chargement, car on émet une µops en moins. De plus, utiliser l'ALU entière permet d'économiser une unité de calcul.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==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. Les unités de calcul sont aussi dupliquées, sauf sur les tout premiers CPU superscalaires historiques, qu'on abordera dans ce qui suit. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 unités de calcul d'un processeur superscalaire===
Le nombre d'unités de calcul dépend fortement des contraintes d’appariement du processeur.
Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif !
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, qui 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 ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée. Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
Implémenter un CPU superscalaire avec une ALU entière, une FPU et une unité mémoire ; est assez simple et on verra comment faire dans la suite. Mais il faut avouer que cela ne donne pas des CPU superscalaires efficaces. En pratique, les CPU superscalaires sont un intermédiaire entre ces deux cas extrêmes : certaines unités de calcul sont dupliquées en plusieurs exemplaires, mais pas toutes. La tendance générale est de dupliquer les unités de calcul entière, le reste n'étant tout simplement pas fait.
Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium. C'était un CPU double émission qui n'implémentait pas la double émission entière-flottante. A la place, il avait deux ALU entières, un multiplieur, une FPU et une unité mémoire. Il pouvait exécuter les combinaisons INT + INT, INT + MUL, et INT + MEM. Par contre, il ne pouvait pas émettre une instruction flottante avec une autre. En clair, rien à voir avec une sorte d'évolution des designs précédents. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
Typiquement, les ALU entières simples sont dupliquées, mais pas le circuit multiplieur, ni le ''barrel shifter''. De moins, sauf sur les processeurs superscalaires très larges. 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. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir exécuter deux multiplications à la fois, idem pour les décalages.
La FPU aussi n'est pas multipliée, ou du moins pas à l'identique. Je fais cette précision car une petite optimisation permet d'émettre deux opérations flottantes sans dupliquer la FPU. La '''double émission FADD/FMUL''' utilise 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. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants, mais ils sont souvent sur le port avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour les unités mémoire, nous mettons cela de côté pour le moment. Mais en attendant, nous allons supposer qu'elle n'est pas dupliquée dans les sections qui vont suivre.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire'''. 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.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
3zdjewby8poe9zplph0h12utr2tvvp3
772824
772823
2026-09-22T14:14:27Z
Mewtow
31375
/* L'émission parallèle des branchements */
772824
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant *toute chose égale par ailleurs*. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ?
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Branchement ||Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Opération entière ||Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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|Émission 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. Avec la seconde, les branchements sont traités comme des instructions entières. Le premier cas est illustré ci-dessous.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
* Unité mémoire
| FPU
| Unité de branchement
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
* µops mémoire.
| Opération flottante
| Branchements
|}
La première solution impose la présence d'une unité de branchement séparée, avec son port d'émission. La seconde peut exécuter les branchements soit dans une ALU entière, soit dans une unité de branchement reliée au port d'émission pour les opérations entières. Elle permet d'économiser beaucoup de transistors dans l'unité d'émission/chargement, car on émet une µops en moins. De plus, utiliser l'ALU entière permet d'économiser une unité de calcul.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==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. Les unités de calcul sont aussi dupliquées, sauf sur les tout premiers CPU superscalaires historiques, qu'on abordera dans ce qui suit. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 unités de calcul d'un processeur superscalaire===
Le nombre d'unités de calcul dépend fortement des contraintes d’appariement du processeur.
Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif !
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, qui 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 ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée. Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
Implémenter un CPU superscalaire avec une ALU entière, une FPU et une unité mémoire ; est assez simple et on verra comment faire dans la suite. Mais il faut avouer que cela ne donne pas des CPU superscalaires efficaces. En pratique, les CPU superscalaires sont un intermédiaire entre ces deux cas extrêmes : certaines unités de calcul sont dupliquées en plusieurs exemplaires, mais pas toutes. La tendance générale est de dupliquer les unités de calcul entière, le reste n'étant tout simplement pas fait.
Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium. C'était un CPU double émission qui n'implémentait pas la double émission entière-flottante. A la place, il avait deux ALU entières, un multiplieur, une FPU et une unité mémoire. Il pouvait exécuter les combinaisons INT + INT, INT + MUL, et INT + MEM. Par contre, il ne pouvait pas émettre une instruction flottante avec une autre. En clair, rien à voir avec une sorte d'évolution des designs précédents. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
Typiquement, les ALU entières simples sont dupliquées, mais pas le circuit multiplieur, ni le ''barrel shifter''. De moins, sauf sur les processeurs superscalaires très larges. 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. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir exécuter deux multiplications à la fois, idem pour les décalages.
La FPU aussi n'est pas multipliée, ou du moins pas à l'identique. Je fais cette précision car une petite optimisation permet d'émettre deux opérations flottantes sans dupliquer la FPU. La '''double émission FADD/FMUL''' utilise 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. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants, mais ils sont souvent sur le port avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour les unités mémoire, nous mettons cela de côté pour le moment. Mais en attendant, nous allons supposer qu'elle n'est pas dupliquée dans les sections qui vont suivre.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire'''. 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.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
d6y0qflqn7up762esl9pou72h83f8uu
772825
772824
2026-09-22T14:20:31Z
Mewtow
31375
/* L'implémentation des processeurs superscalaires */
772825
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant *toute chose égale par ailleurs*. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ?
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Branchement ||Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Opération entière ||Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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|Émission 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. Avec la seconde, les branchements sont traités comme des instructions entières. Le premier cas est illustré ci-dessous.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
* Unité mémoire
| FPU
| Unité de branchement
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
* µops mémoire.
| Opération flottante
| Branchements
|}
La première solution impose la présence d'une unité de branchement séparée, avec son port d'émission. La seconde peut exécuter les branchements soit dans une ALU entière, soit dans une unité de branchement reliée au port d'émission pour les opérations entières. Elle permet d'économiser beaucoup de transistors dans l'unité d'émission/chargement, car on émet une µops en moins. De plus, utiliser l'ALU entière permet d'économiser une unité de calcul.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus atrd. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
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, qui 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 ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée. Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===La duplication des unités fonctionnelles===
Implémenter un CPU superscalaire avec une ALU entière, une FPU et une unité mémoire ; est assez simple et on verra comment faire dans la suite. Mais il faut avouer que cela ne donne pas des CPU superscalaires efficaces. En pratique, les CPU superscalaires sont un intermédiaire entre ces deux cas extrêmes : certaines unités de calcul sont dupliquées en plusieurs exemplaires, mais pas toutes. La tendance générale est de dupliquer les unités de calcul entière, le reste n'étant tout simplement pas fait.
Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium. C'était un CPU double émission qui n'implémentait pas la double émission entière-flottante. A la place, il avait deux ALU entières, un multiplieur, une FPU et une unité mémoire. Il pouvait exécuter les combinaisons INT + INT, INT + MUL, et INT + MEM. Par contre, il ne pouvait pas émettre une instruction flottante avec une autre. En clair, rien à voir avec une sorte d'évolution des designs précédents. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
Typiquement, les ALU entières simples sont dupliquées, mais pas le circuit multiplieur, ni le ''barrel shifter''. De moins, sauf sur les processeurs superscalaires très larges. 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. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir exécuter deux multiplications à la fois, idem pour les décalages.
La FPU aussi n'est pas multipliée, ou du moins pas à l'identique. Je fais cette précision car une petite optimisation permet d'émettre deux opérations flottantes sans dupliquer la FPU. La '''double émission FADD/FMUL''' utilise 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. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants, mais ils sont souvent sur le port avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour les unités mémoire, nous mettons cela de côté pour le moment. Mais en attendant, nous allons supposer qu'elle n'est pas dupliquée dans les sections qui vont suivre.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
==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. Les unités de calcul sont aussi dupliquées, sauf sur les tout premiers CPU superscalaires historiques, qu'on abordera dans ce qui suit. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire'''. 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.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
nz42arcrnjfbeowl8qvxcoiwzb5grjs
772826
772825
2026-09-22T14:20:56Z
Mewtow
31375
/* L'implémentation des processeurs superscalaires */
772826
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant *toute chose égale par ailleurs*. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ?
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Branchement ||Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Opération entière ||Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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|Émission 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. Avec la seconde, les branchements sont traités comme des instructions entières. Le premier cas est illustré ci-dessous.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
* Unité mémoire
| FPU
| Unité de branchement
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
* µops mémoire.
| Opération flottante
| Branchements
|}
La première solution impose la présence d'une unité de branchement séparée, avec son port d'émission. La seconde peut exécuter les branchements soit dans une ALU entière, soit dans une unité de branchement reliée au port d'émission pour les opérations entières. Elle permet d'économiser beaucoup de transistors dans l'unité d'émission/chargement, car on émet une µops en moins. De plus, utiliser l'ALU entière permet d'économiser une unité de calcul.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus atrd. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
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, qui 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 ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée. Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===La duplication des unités fonctionnelles===
Implémenter un CPU superscalaire avec une ALU entière, une FPU et une unité mémoire ; est assez simple et on verra comment faire dans la suite. Mais il faut avouer que cela ne donne pas des CPU superscalaires efficaces. En pratique, les CPU superscalaires sont un intermédiaire entre ces deux cas extrêmes : certaines unités de calcul sont dupliquées en plusieurs exemplaires, mais pas toutes. La tendance générale est de dupliquer les unités de calcul entière, le reste n'étant tout simplement pas fait.
Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium. C'était un CPU double émission qui n'implémentait pas la double émission entière-flottante. A la place, il avait deux ALU entières, un multiplieur, une FPU et une unité mémoire. Il pouvait exécuter les combinaisons INT + INT, INT + MUL, et INT + MEM. Par contre, il ne pouvait pas émettre une instruction flottante avec une autre. En clair, rien à voir avec une sorte d'évolution des designs précédents. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
Typiquement, les ALU entières simples sont dupliquées, mais pas le circuit multiplieur, ni le ''barrel shifter''. De moins, sauf sur les processeurs superscalaires très larges. 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. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir exécuter deux multiplications à la fois, idem pour les décalages.
La FPU aussi n'est pas multipliée, ou du moins pas à l'identique. Je fais cette précision car une petite optimisation permet d'émettre deux opérations flottantes sans dupliquer la FPU. La '''double émission FADD/FMUL''' utilise 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. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants, mais ils sont souvent sur le port avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour les unités mémoire, nous mettons cela de côté pour le moment. Mais en attendant, nous allons supposer qu'elle n'est pas dupliquée dans les sections qui vont suivre.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire'''. 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.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
62mwc84dh30ldcj3va7ici8e83hb21n
772827
772826
2026-09-22T14:31:32Z
Mewtow
31375
/* Les unités de calcul d'un processeur superscalaire */
772827
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant *toute chose égale par ailleurs*. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ?
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Branchement ||Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Opération entière ||Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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|Émission 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. Avec la seconde, les branchements sont traités comme des instructions entières. Le premier cas est illustré ci-dessous.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
* Unité mémoire
| FPU
| Unité de branchement
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
* µops mémoire.
| Opération flottante
| Branchements
|}
La première solution impose la présence d'une unité de branchement séparée, avec son port d'émission. La seconde peut exécuter les branchements soit dans une ALU entière, soit dans une unité de branchement reliée au port d'émission pour les opérations entières. Elle permet d'économiser beaucoup de transistors dans l'unité d'émission/chargement, car on émet une µops en moins. De plus, utiliser l'ALU entière permet d'économiser une unité de calcul.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus atrd. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qui peut être complété avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement''' des unités fonctionnelles. Le terme n'est pas beaucoup utilisé, mais quelques vieux articles académiques l'utilisent. Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien sparer instructions entières, flottantes, branchements et instructions mémoire.
Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée. Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===La duplication des unités fonctionnelles===
Implémenter un CPU superscalaire avec une ALU entière, une FPU et une unité mémoire ; est assez simple et on verra comment faire dans la suite. Mais il faut avouer que cela ne donne pas des CPU superscalaires efficaces. En pratique, les CPU superscalaires sont un intermédiaire entre ces deux cas extrêmes : certaines unités de calcul sont dupliquées en plusieurs exemplaires, mais pas toutes. La tendance générale est de dupliquer les unités de calcul entière, le reste n'étant tout simplement pas fait.
Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium. C'était un CPU double émission qui n'implémentait pas la double émission entière-flottante. A la place, il avait deux ALU entières, un multiplieur, une FPU et une unité mémoire. Il pouvait exécuter les combinaisons INT + INT, INT + MUL, et INT + MEM. Par contre, il ne pouvait pas émettre une instruction flottante avec une autre. En clair, rien à voir avec une sorte d'évolution des designs précédents. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
Typiquement, les ALU entières simples sont dupliquées, mais pas le circuit multiplieur, ni le ''barrel shifter''. De moins, sauf sur les processeurs superscalaires très larges. 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. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir exécuter deux multiplications à la fois, idem pour les décalages.
La FPU aussi n'est pas multipliée, ou du moins pas à l'identique. Je fais cette précision car une petite optimisation permet d'émettre deux opérations flottantes sans dupliquer la FPU. La '''double émission FADD/FMUL''' utilise 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. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants, mais ils sont souvent sur le port avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour les unités mémoire, nous mettons cela de côté pour le moment. Mais en attendant, nous allons supposer qu'elle n'est pas dupliquée dans les sections qui vont suivre.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire'''. 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.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
g8yzk0653gwp6v9gm0j22vlm0b80v7s
772828
772827
2026-09-22T14:32:56Z
Mewtow
31375
/* L'émission parallèle des accès mémoire */
772828
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant *toute chose égale par ailleurs*. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ?
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Branchement ||Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Opération entière ||Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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|Émission 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. Avec la seconde, les branchements sont traités comme des instructions entières. Le premier cas est illustré ci-dessous.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
* Unité mémoire
| FPU
| Unité de branchement
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
* µops mémoire.
| Opération flottante
| Branchements
|}
La première solution impose la présence d'une unité de branchement séparée, avec son port d'émission. La seconde peut exécuter les branchements soit dans une ALU entière, soit dans une unité de branchement reliée au port d'émission pour les opérations entières. Elle permet d'économiser beaucoup de transistors dans l'unité d'émission/chargement, car on émet une µops en moins. De plus, utiliser l'ALU entière permet d'économiser une unité de calcul.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus atrd. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qui peut être complété avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement''' des unités fonctionnelles. Le terme n'est pas beaucoup utilisé, mais quelques vieux articles académiques l'utilisent. Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien sparer instructions entières, flottantes, branchements et instructions mémoire.
Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée. Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===La duplication des unités fonctionnelles===
Implémenter un CPU superscalaire avec une ALU entière, une FPU et une unité mémoire ; est assez simple et on verra comment faire dans la suite. Mais il faut avouer que cela ne donne pas des CPU superscalaires efficaces. En pratique, les CPU superscalaires sont un intermédiaire entre ces deux cas extrêmes : certaines unités de calcul sont dupliquées en plusieurs exemplaires, mais pas toutes. La tendance générale est de dupliquer les unités de calcul entière, le reste n'étant tout simplement pas fait.
Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium. C'était un CPU double émission qui n'implémentait pas la double émission entière-flottante. A la place, il avait deux ALU entières, un multiplieur, une FPU et une unité mémoire. Il pouvait exécuter les combinaisons INT + INT, INT + MUL, et INT + MEM. Par contre, il ne pouvait pas émettre une instruction flottante avec une autre. En clair, rien à voir avec une sorte d'évolution des designs précédents. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
Typiquement, les ALU entières simples sont dupliquées, mais pas le circuit multiplieur, ni le ''barrel shifter''. De moins, sauf sur les processeurs superscalaires très larges. 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. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir exécuter deux multiplications à la fois, idem pour les décalages.
La FPU aussi n'est pas multipliée, ou du moins pas à l'identique. Je fais cette précision car une petite optimisation permet d'émettre deux opérations flottantes sans dupliquer la FPU. La '''double émission FADD/FMUL''' utilise 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. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants, mais ils sont souvent sur le port avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour les unités mémoire, nous mettons cela de côté pour le moment. Mais en attendant, nous allons supposer qu'elle n'est pas dupliquée dans les sections qui vont suivre.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
3gtbjf8u9rtbhlpxj39ft33e42qeyb7
772829
772828
2026-09-22T14:36:37Z
Mewtow
31375
/* Le partitionnement des unités fonctionnelles */
772829
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant *toute chose égale par ailleurs*. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ?
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Branchement ||Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Opération entière ||Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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|Émission 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. Avec la seconde, les branchements sont traités comme des instructions entières. Le premier cas est illustré ci-dessous.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
* Unité mémoire
| FPU
| Unité de branchement
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
* µops mémoire.
| Opération flottante
| Branchements
|}
La première solution impose la présence d'une unité de branchement séparée, avec son port d'émission. La seconde peut exécuter les branchements soit dans une ALU entière, soit dans une unité de branchement reliée au port d'émission pour les opérations entières. Elle permet d'économiser beaucoup de transistors dans l'unité d'émission/chargement, car on émet une µops en moins. De plus, utiliser l'ALU entière permet d'économiser une unité de calcul.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus atrd. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qui peut être complété avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement''' des unités fonctionnelles. Le terme n'est pas beaucoup utilisé, mais quelques vieux articles académiques l'utilisent. Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Pour ce qui est du gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles. Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===La duplication des unités fonctionnelles===
Implémenter un CPU superscalaire avec une ALU entière, une FPU et une unité mémoire ; est assez simple et on verra comment faire dans la suite. Mais il faut avouer que cela ne donne pas des CPU superscalaires efficaces. En pratique, les CPU superscalaires sont un intermédiaire entre ces deux cas extrêmes : certaines unités de calcul sont dupliquées en plusieurs exemplaires, mais pas toutes. La tendance générale est de dupliquer les unités de calcul entière, le reste n'étant tout simplement pas fait.
Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium. C'était un CPU double émission qui n'implémentait pas la double émission entière-flottante. A la place, il avait deux ALU entières, un multiplieur, une FPU et une unité mémoire. Il pouvait exécuter les combinaisons INT + INT, INT + MUL, et INT + MEM. Par contre, il ne pouvait pas émettre une instruction flottante avec une autre. En clair, rien à voir avec une sorte d'évolution des designs précédents. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
Typiquement, les ALU entières simples sont dupliquées, mais pas le circuit multiplieur, ni le ''barrel shifter''. De moins, sauf sur les processeurs superscalaires très larges. 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. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir exécuter deux multiplications à la fois, idem pour les décalages.
La FPU aussi n'est pas multipliée, ou du moins pas à l'identique. Je fais cette précision car une petite optimisation permet d'émettre deux opérations flottantes sans dupliquer la FPU. La '''double émission FADD/FMUL''' utilise 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. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants, mais ils sont souvent sur le port avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour les unités mémoire, nous mettons cela de côté pour le moment. Mais en attendant, nous allons supposer qu'elle n'est pas dupliquée dans les sections qui vont suivre.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
j9w66kmnwftpvo9iqtztfut811m6l5a
772830
772829
2026-09-22T14:41:41Z
Mewtow
31375
/* L'émission parallèle des branchements */
772830
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant *toute chose égale par ailleurs*. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ?
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Branchement ||Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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, l'HyperSPARC, les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Opération entière ||Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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|Émission 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. Avec la seconde, les branchements sont traités comme des instructions entières. Le premier cas est illustré ci-dessous.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
* Unité mémoire
| FPU
| Unité de branchement
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
* µops mémoire.
| Opération flottante
| Branchements
|}
La première solution impose la présence d'une unité de branchement séparée, avec son port d'émission. La seconde peut exécuter les branchements soit dans une ALU entière, soit dans une unité de branchement reliée au port d'émission pour les opérations entières. Elle permet d'économiser beaucoup de transistors dans l'unité d'émission/chargement, car on émet une µops en moins. De plus, utiliser l'ALU entière permet d'économiser une unité de calcul.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus atrd. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qui peut être complété avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement''' des unités fonctionnelles. Le terme n'est pas beaucoup utilisé, mais quelques vieux articles académiques l'utilisent. Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Pour ce qui est du gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles. Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===La duplication des unités fonctionnelles===
Implémenter un CPU superscalaire avec une ALU entière, une FPU et une unité mémoire ; est assez simple et on verra comment faire dans la suite. Mais il faut avouer que cela ne donne pas des CPU superscalaires efficaces. En pratique, les CPU superscalaires sont un intermédiaire entre ces deux cas extrêmes : certaines unités de calcul sont dupliquées en plusieurs exemplaires, mais pas toutes. La tendance générale est de dupliquer les unités de calcul entière, le reste n'étant tout simplement pas fait.
Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium. C'était un CPU double émission qui n'implémentait pas la double émission entière-flottante. A la place, il avait deux ALU entières, un multiplieur, une FPU et une unité mémoire. Il pouvait exécuter les combinaisons INT + INT, INT + MUL, et INT + MEM. Par contre, il ne pouvait pas émettre une instruction flottante avec une autre. En clair, rien à voir avec une sorte d'évolution des designs précédents. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
Typiquement, les ALU entières simples sont dupliquées, mais pas le circuit multiplieur, ni le ''barrel shifter''. De moins, sauf sur les processeurs superscalaires très larges. 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. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir exécuter deux multiplications à la fois, idem pour les décalages.
La FPU aussi n'est pas multipliée, ou du moins pas à l'identique. Je fais cette précision car une petite optimisation permet d'émettre deux opérations flottantes sans dupliquer la FPU. La '''double émission FADD/FMUL''' utilise 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. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants, mais ils sont souvent sur le port avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour les unités mémoire, nous mettons cela de côté pour le moment. Mais en attendant, nous allons supposer qu'elle n'est pas dupliquée dans les sections qui vont suivre.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
seqrys6knzbrerbo5u6jgan7oz7xq01
772831
772830
2026-09-22T14:42:46Z
Mewtow
31375
/* La double émission entière-flottante */
772831
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant *toute chose égale par ailleurs*. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ?
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Branchement ||Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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, l'HyperSPARC, les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Opération entière ||Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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|Émission 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. Avec la seconde, les branchements sont traités comme des instructions entières. Le premier cas est illustré ci-dessous.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
* Unité mémoire
| FPU
| Unité de branchement
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
* µops mémoire.
| Opération flottante
| Branchements
|}
La première solution impose la présence d'une unité de branchement séparée, avec son port d'émission. La seconde peut exécuter les branchements soit dans une ALU entière, soit dans une unité de branchement reliée au port d'émission pour les opérations entières. Elle permet d'économiser beaucoup de transistors dans l'unité d'émission/chargement, car on émet une µops en moins. De plus, utiliser l'ALU entière permet d'économiser une unité de calcul.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus atrd. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qui peut être complété avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement''' des unités fonctionnelles. Le terme n'est pas beaucoup utilisé, mais quelques vieux articles académiques l'utilisent. Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Pour ce qui est du gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles. Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===La duplication des unités fonctionnelles===
Implémenter un CPU superscalaire avec une ALU entière, une FPU et une unité mémoire ; est assez simple et on verra comment faire dans la suite. Mais il faut avouer que cela ne donne pas des CPU superscalaires efficaces. En pratique, les CPU superscalaires sont un intermédiaire entre ces deux cas extrêmes : certaines unités de calcul sont dupliquées en plusieurs exemplaires, mais pas toutes. La tendance générale est de dupliquer les unités de calcul entière, le reste n'étant tout simplement pas fait.
Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium. C'était un CPU double émission qui n'implémentait pas la double émission entière-flottante. A la place, il avait deux ALU entières, un multiplieur, une FPU et une unité mémoire. Il pouvait exécuter les combinaisons INT + INT, INT + MUL, et INT + MEM. Par contre, il ne pouvait pas émettre une instruction flottante avec une autre. En clair, rien à voir avec une sorte d'évolution des designs précédents. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
Typiquement, les ALU entières simples sont dupliquées, mais pas le circuit multiplieur, ni le ''barrel shifter''. De moins, sauf sur les processeurs superscalaires très larges. 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. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir exécuter deux multiplications à la fois, idem pour les décalages.
La FPU aussi n'est pas multipliée, ou du moins pas à l'identique. Je fais cette précision car une petite optimisation permet d'émettre deux opérations flottantes sans dupliquer la FPU. La '''double émission FADD/FMUL''' utilise 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. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants, mais ils sont souvent sur le port avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour les unités mémoire, nous mettons cela de côté pour le moment. Mais en attendant, nous allons supposer qu'elle n'est pas dupliquée dans les sections qui vont suivre.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
onqnjfizij14u69j29qu79oxyli0v76
772832
772831
2026-09-22T14:44:24Z
Mewtow
31375
/* Le partitionnement des unités fonctionnelles */
772832
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant *toute chose égale par ailleurs*. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ?
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Branchement ||Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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, l'HyperSPARC, les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Opération entière ||Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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|Émission 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. Avec la seconde, les branchements sont traités comme des instructions entières. Le premier cas est illustré ci-dessous.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
* Unité mémoire
| FPU
| Unité de branchement
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
* µops mémoire.
| Opération flottante
| Branchements
|}
La première solution impose la présence d'une unité de branchement séparée, avec son port d'émission. La seconde peut exécuter les branchements soit dans une ALU entière, soit dans une unité de branchement reliée au port d'émission pour les opérations entières. Elle permet d'économiser beaucoup de transistors dans l'unité d'émission/chargement, car on émet une µops en moins. De plus, utiliser l'ALU entière permet d'économiser une unité de calcul.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus atrd. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement''' des unités fonctionnelles. Le terme n'est pas beaucoup utilisé, mais quelques vieux articles académiques l'utilisent. Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Pour ce qui est du gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles. Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===La duplication des unités fonctionnelles===
Implémenter un CPU superscalaire avec une ALU entière, une FPU et une unité mémoire ; est assez simple et on verra comment faire dans la suite. Mais il faut avouer que cela ne donne pas des CPU superscalaires efficaces. En pratique, les CPU superscalaires sont un intermédiaire entre ces deux cas extrêmes : certaines unités de calcul sont dupliquées en plusieurs exemplaires, mais pas toutes. La tendance générale est de dupliquer les unités de calcul entière, le reste n'étant tout simplement pas fait.
Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium. C'était un CPU double émission qui n'implémentait pas la double émission entière-flottante. A la place, il avait deux ALU entières, un multiplieur, une FPU et une unité mémoire. Il pouvait exécuter les combinaisons INT + INT, INT + MUL, et INT + MEM. Par contre, il ne pouvait pas émettre une instruction flottante avec une autre. En clair, rien à voir avec une sorte d'évolution des designs précédents. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
Typiquement, les ALU entières simples sont dupliquées, mais pas le circuit multiplieur, ni le ''barrel shifter''. De moins, sauf sur les processeurs superscalaires très larges. 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. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir exécuter deux multiplications à la fois, idem pour les décalages.
La FPU aussi n'est pas multipliée, ou du moins pas à l'identique. Je fais cette précision car une petite optimisation permet d'émettre deux opérations flottantes sans dupliquer la FPU. La '''double émission FADD/FMUL''' utilise 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. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants, mais ils sont souvent sur le port avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour les unités mémoire, nous mettons cela de côté pour le moment. Mais en attendant, nous allons supposer qu'elle n'est pas dupliquée dans les sections qui vont suivre.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
04zqr9b7snnvcfig2v222s7qyerb9ls
772833
772832
2026-09-22T14:44:43Z
Mewtow
31375
/* Les unités de calcul d'un processeur superscalaire */
772833
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant *toute chose égale par ailleurs*. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ?
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Branchement ||Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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, l'HyperSPARC, les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Opération entière ||Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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|Émission 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. Avec la seconde, les branchements sont traités comme des instructions entières. Le premier cas est illustré ci-dessous.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
* Unité mémoire
| FPU
| Unité de branchement
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
* µops mémoire.
| Opération flottante
| Branchements
|}
La première solution impose la présence d'une unité de branchement séparée, avec son port d'émission. La seconde peut exécuter les branchements soit dans une ALU entière, soit dans une unité de branchement reliée au port d'émission pour les opérations entières. Elle permet d'économiser beaucoup de transistors dans l'unité d'émission/chargement, car on émet une µops en moins. De plus, utiliser l'ALU entière permet d'économiser une unité de calcul.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus atrd. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement''' des unités fonctionnelles. Le terme n'est pas beaucoup utilisé, mais quelques vieux articles académiques l'utilisent. Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Pour ce qui est du gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles. Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===La duplication des unités fonctionnelles===
Implémenter un CPU superscalaire avec une ALU entière, une FPU et une unité mémoire ; est assez simple et on verra comment faire dans la suite. Mais il faut avouer que cela ne donne pas des CPU superscalaires efficaces. En pratique, les CPU superscalaires sont un intermédiaire entre ces deux cas extrêmes : certaines unités de calcul sont dupliquées en plusieurs exemplaires, mais pas toutes. La tendance générale est de dupliquer les unités de calcul entière, le reste n'étant tout simplement pas fait.
Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium. C'était un CPU double émission qui n'implémentait pas la double émission entière-flottante. A la place, il avait deux ALU entières, un multiplieur, une FPU et une unité mémoire. Il pouvait exécuter les combinaisons INT + INT, INT + MUL, et INT + MEM. Par contre, il ne pouvait pas émettre une instruction flottante avec une autre. En clair, rien à voir avec une sorte d'évolution des designs précédents. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
Typiquement, les ALU entières simples sont dupliquées, mais pas le circuit multiplieur, ni le ''barrel shifter''. De moins, sauf sur les processeurs superscalaires très larges. 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. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir exécuter deux multiplications à la fois, idem pour les décalages.
La FPU aussi n'est pas multipliée, ou du moins pas à l'identique.
Pour les unités mémoire, nous mettons cela de côté pour le moment. Mais en attendant, nous allons supposer qu'elle n'est pas dupliquée dans les sections qui vont suivre.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
d8v38wd0wch7zmrjpj4m6k56d6gp1uy
772834
772833
2026-09-22T14:53:00Z
Mewtow
31375
/* Le partitionnement des unités fonctionnelles */
772834
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant *toute chose égale par ailleurs*. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ?
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Branchement ||Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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, l'HyperSPARC, les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Opération entière ||Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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|Émission 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. Avec la seconde, les branchements sont traités comme des instructions entières. Le premier cas est illustré ci-dessous.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
* Unité mémoire
| FPU
| Unité de branchement
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
* µops mémoire.
| Opération flottante
| Branchements
|}
La première solution impose la présence d'une unité de branchement séparée, avec son port d'émission. La seconde peut exécuter les branchements soit dans une ALU entière, soit dans une unité de branchement reliée au port d'émission pour les opérations entières. Elle permet d'économiser beaucoup de transistors dans l'unité d'émission/chargement, car on émet une µops en moins. De plus, utiliser l'ALU entière permet d'économiser une unité de calcul.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus atrd. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement''' des unités fonctionnelles. Le terme n'est pas beaucoup utilisé, mais quelques vieux articles académiques l'utilisent. Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Pour ce qui est du gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles. Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit. Mais en pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel.
===La duplication des unités fonctionnelles===
Implémenter un CPU superscalaire avec une ALU entière, une FPU et une unité mémoire ; est assez simple et on verra comment faire dans la suite. Mais il faut avouer que cela ne donne pas des CPU superscalaires efficaces. En pratique, les CPU superscalaires sont un intermédiaire entre ces deux cas extrêmes : certaines unités de calcul sont dupliquées en plusieurs exemplaires, mais pas toutes. La tendance générale est de dupliquer les unités de calcul entière, le reste n'étant tout simplement pas fait.
Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium. C'était un CPU double émission qui n'implémentait pas la double émission entière-flottante. A la place, il avait deux ALU entières, un multiplieur, une FPU et une unité mémoire. Il pouvait exécuter les combinaisons INT + INT, INT + MUL, et INT + MEM. Par contre, il ne pouvait pas émettre une instruction flottante avec une autre. En clair, rien à voir avec une sorte d'évolution des designs précédents. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
Typiquement, les ALU entières simples sont dupliquées, mais pas le circuit multiplieur, ni le ''barrel shifter''. De moins, sauf sur les processeurs superscalaires très larges. 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. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir exécuter deux multiplications à la fois, idem pour les décalages.
La FPU aussi n'est pas multipliée, ou du moins pas à l'identique.
Pour les unités mémoire, nous mettons cela de côté pour le moment. Mais en attendant, nous allons supposer qu'elle n'est pas dupliquée dans les sections qui vont suivre.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
s0ks69ojcrsl5utl004vl75cacll7wn
772835
772834
2026-09-22T14:55:17Z
Mewtow
31375
/* Le partitionnement des unités fonctionnelles */
772835
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant *toute chose égale par ailleurs*. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ?
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Branchement ||Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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, l'HyperSPARC, les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Opération entière ||Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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|Émission 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. Avec la seconde, les branchements sont traités comme des instructions entières. Le premier cas est illustré ci-dessous.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
* Unité mémoire
| FPU
| Unité de branchement
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
* µops mémoire.
| Opération flottante
| Branchements
|}
La première solution impose la présence d'une unité de branchement séparée, avec son port d'émission. La seconde peut exécuter les branchements soit dans une ALU entière, soit dans une unité de branchement reliée au port d'émission pour les opérations entières. Elle permet d'économiser beaucoup de transistors dans l'unité d'émission/chargement, car on émet une µops en moins. De plus, utiliser l'ALU entière permet d'économiser une unité de calcul.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus atrd. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement''' des unités fonctionnelles, aussi appelé le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé, mais quelques vieux articles académiques l'utilisent. Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Pour ce qui est du gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles. Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit. Mais en pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel.
===La duplication des unités fonctionnelles===
Implémenter un CPU superscalaire avec une ALU entière, une FPU et une unité mémoire ; est assez simple et on verra comment faire dans la suite. Mais il faut avouer que cela ne donne pas des CPU superscalaires efficaces. En pratique, les CPU superscalaires sont un intermédiaire entre ces deux cas extrêmes : certaines unités de calcul sont dupliquées en plusieurs exemplaires, mais pas toutes. La tendance générale est de dupliquer les unités de calcul entière, le reste n'étant tout simplement pas fait.
Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium. C'était un CPU double émission qui n'implémentait pas la double émission entière-flottante. A la place, il avait deux ALU entières, un multiplieur, une FPU et une unité mémoire. Il pouvait exécuter les combinaisons INT + INT, INT + MUL, et INT + MEM. Par contre, il ne pouvait pas émettre une instruction flottante avec une autre. En clair, rien à voir avec une sorte d'évolution des designs précédents. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
Typiquement, les ALU entières simples sont dupliquées, mais pas le circuit multiplieur, ni le ''barrel shifter''. De moins, sauf sur les processeurs superscalaires très larges. 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. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir exécuter deux multiplications à la fois, idem pour les décalages.
La FPU aussi n'est pas multipliée, ou du moins pas à l'identique.
Pour les unités mémoire, nous mettons cela de côté pour le moment. Mais en attendant, nous allons supposer qu'elle n'est pas dupliquée dans les sections qui vont suivre.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
5k5d1hxcvzqzo6bzkabtwsdyh4wts3b
772836
772835
2026-09-22T14:56:10Z
Mewtow
31375
/* Le partitionnement intra-unité */
772836
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant *toute chose égale par ailleurs*. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ?
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Branchement ||Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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, l'HyperSPARC, les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Opération entière ||Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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|Émission 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. Avec la seconde, les branchements sont traités comme des instructions entières. Le premier cas est illustré ci-dessous.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
* Unité mémoire
| FPU
| Unité de branchement
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
* µops mémoire.
| Opération flottante
| Branchements
|}
La première solution impose la présence d'une unité de branchement séparée, avec son port d'émission. La seconde peut exécuter les branchements soit dans une ALU entière, soit dans une unité de branchement reliée au port d'émission pour les opérations entières. Elle permet d'économiser beaucoup de transistors dans l'unité d'émission/chargement, car on émet une µops en moins. De plus, utiliser l'ALU entière permet d'économiser une unité de calcul.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus atrd. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement''' des unités fonctionnelles, aussi appelé le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé, mais quelques vieux articles académiques l'utilisent. Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Pour ce qui est du gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles. Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duiplication des unités entières.
===La duplication des unités fonctionnelles===
Implémenter un CPU superscalaire avec une ALU entière, une FPU et une unité mémoire ; est assez simple et on verra comment faire dans la suite. Mais il faut avouer que cela ne donne pas des CPU superscalaires efficaces. En pratique, les CPU superscalaires sont un intermédiaire entre ces deux cas extrêmes : certaines unités de calcul sont dupliquées en plusieurs exemplaires, mais pas toutes. La tendance générale est de dupliquer les unités de calcul entière, le reste n'étant tout simplement pas fait.
Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium. C'était un CPU double émission qui n'implémentait pas la double émission entière-flottante. A la place, il avait deux ALU entières, un multiplieur, une FPU et une unité mémoire. Il pouvait exécuter les combinaisons INT + INT, INT + MUL, et INT + MEM. Par contre, il ne pouvait pas émettre une instruction flottante avec une autre. En clair, rien à voir avec une sorte d'évolution des designs précédents. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
Typiquement, les ALU entières simples sont dupliquées, mais pas le circuit multiplieur, ni le ''barrel shifter''. De moins, sauf sur les processeurs superscalaires très larges. 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. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir exécuter deux multiplications à la fois, idem pour les décalages.
La FPU aussi n'est pas multipliée, ou du moins pas à l'identique.
Pour les unités mémoire, nous mettons cela de côté pour le moment. Mais en attendant, nous allons supposer qu'elle n'est pas dupliquée dans les sections qui vont suivre.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
clhc7f2ts54hmf8o1747lwj8vpdf34k
772837
772836
2026-09-22T15:18:21Z
Mewtow
31375
/* Le partitionnement intra-unité */
772837
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant *toute chose égale par ailleurs*. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ?
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Branchement ||Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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, l'HyperSPARC, les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Opération entière ||Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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|Émission 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. Avec la seconde, les branchements sont traités comme des instructions entières. Le premier cas est illustré ci-dessous.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
* Unité mémoire
| FPU
| Unité de branchement
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
* µops mémoire.
| Opération flottante
| Branchements
|}
La première solution impose la présence d'une unité de branchement séparée, avec son port d'émission. La seconde peut exécuter les branchements soit dans une ALU entière, soit dans une unité de branchement reliée au port d'émission pour les opérations entières. Elle permet d'économiser beaucoup de transistors dans l'unité d'émission/chargement, car on émet une µops en moins. De plus, utiliser l'ALU entière permet d'économiser une unité de calcul.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus atrd. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement''' des unités fonctionnelles, aussi appelé le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé, mais quelques vieux articles académiques l'utilisent. Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Pour ce qui est du gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles. Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duiplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse. En soi, rien de compliqué. Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
===La duplication des unités fonctionnelles===
Implémenter un CPU superscalaire avec une ALU entière, une FPU et une unité mémoire ; est assez simple et on verra comment faire dans la suite. Mais il faut avouer que cela ne donne pas des CPU superscalaires efficaces. En pratique, les CPU superscalaires sont un intermédiaire entre ces deux cas extrêmes : certaines unités de calcul sont dupliquées en plusieurs exemplaires, mais pas toutes. La tendance générale est de dupliquer les unités de calcul entière, le reste n'étant tout simplement pas fait.
Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium. C'était un CPU double émission qui n'implémentait pas la double émission entière-flottante. A la place, il avait deux ALU entières, un multiplieur, une FPU et une unité mémoire. Il pouvait exécuter les combinaisons INT + INT, INT + MUL, et INT + MEM. Par contre, il ne pouvait pas émettre une instruction flottante avec une autre. En clair, rien à voir avec une sorte d'évolution des designs précédents. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
Typiquement, les ALU entières simples sont dupliquées, mais pas le circuit multiplieur, ni le ''barrel shifter''. De moins, sauf sur les processeurs superscalaires très larges. 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. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir exécuter deux multiplications à la fois, idem pour les décalages.
La FPU aussi n'est pas multipliée, ou du moins pas à l'identique.
Pour les unités mémoire, nous mettons cela de côté pour le moment. Mais en attendant, nous allons supposer qu'elle n'est pas dupliquée dans les sections qui vont suivre.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
b79j0a9izaso9s4srq58klfq0znfqkr
772838
772837
2026-09-22T15:24:20Z
Mewtow
31375
/* Le partitionnement intra-unité */
772838
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant *toute chose égale par ailleurs*. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ?
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Branchement ||Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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, l'HyperSPARC, les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Opération entière ||Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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|Émission 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. Avec la seconde, les branchements sont traités comme des instructions entières. Le premier cas est illustré ci-dessous.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
* Unité mémoire
| FPU
| Unité de branchement
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
* µops mémoire.
| Opération flottante
| Branchements
|}
La première solution impose la présence d'une unité de branchement séparée, avec son port d'émission. La seconde peut exécuter les branchements soit dans une ALU entière, soit dans une unité de branchement reliée au port d'émission pour les opérations entières. Elle permet d'économiser beaucoup de transistors dans l'unité d'émission/chargement, car on émet une µops en moins. De plus, utiliser l'ALU entière permet d'économiser une unité de calcul.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus atrd. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement''' des unités fonctionnelles, aussi appelé le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé, mais quelques vieux articles académiques l'utilisent. Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Pour ce qui est du gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles. Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duiplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse. En soi, rien de compliqué. Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
===La duplication des unités fonctionnelles===
Implémenter un CPU superscalaire avec une ALU entière, une FPU et une unité mémoire ; est assez simple et on verra comment faire dans la suite. Mais il faut avouer que cela ne donne pas des CPU superscalaires efficaces. En pratique, les CPU superscalaires sont un intermédiaire entre ces deux cas extrêmes : certaines unités de calcul sont dupliquées en plusieurs exemplaires, mais pas toutes. La tendance générale est de dupliquer les unités de calcul entière, le reste n'étant tout simplement pas fait.
Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium. C'était un CPU double émission qui n'implémentait pas la double émission entière-flottante. A la place, il avait deux ALU entières, un multiplieur, une FPU et une unité mémoire. Il pouvait exécuter les combinaisons INT + INT, INT + MUL, et INT + MEM. Par contre, il ne pouvait pas émettre une instruction flottante avec une autre. En clair, rien à voir avec une sorte d'évolution des designs précédents. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
Typiquement, les ALU entières simples sont dupliquées, mais pas le circuit multiplieur, ni le ''barrel shifter''. De moins, sauf sur les processeurs superscalaires très larges. 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. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir exécuter deux multiplications à la fois, idem pour les décalages.
La FPU aussi n'est pas multipliée, ou du moins pas à l'identique.
Pour les unités mémoire, nous mettons cela de côté pour le moment. Mais en attendant, nous allons supposer qu'elle n'est pas dupliquée dans les sections qui vont suivre.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
3sf4c8xhq84twj8u7gf0wshptypfnvg
772839
772838
2026-09-22T15:25:44Z
Mewtow
31375
/* Le partitionnement intra-unité */
772839
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant *toute chose égale par ailleurs*. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ?
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Branchement ||Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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, l'HyperSPARC, les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Opération entière ||Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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|Émission 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. Avec la seconde, les branchements sont traités comme des instructions entières. Le premier cas est illustré ci-dessous.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
* Unité mémoire
| FPU
| Unité de branchement
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
* µops mémoire.
| Opération flottante
| Branchements
|}
La première solution impose la présence d'une unité de branchement séparée, avec son port d'émission. La seconde peut exécuter les branchements soit dans une ALU entière, soit dans une unité de branchement reliée au port d'émission pour les opérations entières. Elle permet d'économiser beaucoup de transistors dans l'unité d'émission/chargement, car on émet une µops en moins. De plus, utiliser l'ALU entière permet d'économiser une unité de calcul.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus atrd. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement''' des unités fonctionnelles, aussi appelé le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé, mais quelques vieux articles académiques l'utilisent. Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Pour ce qui est du gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles. Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duiplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement de l'unité mémoire comme ALU entière===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse. En soi, rien de compliqué. Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
===La duplication des unités fonctionnelles===
Implémenter un CPU superscalaire avec une ALU entière, une FPU et une unité mémoire ; est assez simple et on verra comment faire dans la suite. Mais il faut avouer que cela ne donne pas des CPU superscalaires efficaces. En pratique, les CPU superscalaires sont un intermédiaire entre ces deux cas extrêmes : certaines unités de calcul sont dupliquées en plusieurs exemplaires, mais pas toutes. La tendance générale est de dupliquer les unités de calcul entière, le reste n'étant tout simplement pas fait.
Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium. C'était un CPU double émission qui n'implémentait pas la double émission entière-flottante. A la place, il avait deux ALU entières, un multiplieur, une FPU et une unité mémoire. Il pouvait exécuter les combinaisons INT + INT, INT + MUL, et INT + MEM. Par contre, il ne pouvait pas émettre une instruction flottante avec une autre. En clair, rien à voir avec une sorte d'évolution des designs précédents. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
Typiquement, les ALU entières simples sont dupliquées, mais pas le circuit multiplieur, ni le ''barrel shifter''. De moins, sauf sur les processeurs superscalaires très larges. 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. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir exécuter deux multiplications à la fois, idem pour les décalages.
La FPU aussi n'est pas multipliée, ou du moins pas à l'identique.
Pour les unités mémoire, nous mettons cela de côté pour le moment. Mais en attendant, nous allons supposer qu'elle n'est pas dupliquée dans les sections qui vont suivre.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
8tbr6vmypvhjnzfqlm4qoyhomu3zyec
772840
772839
2026-09-22T15:44:11Z
Mewtow
31375
/* La duplication des unités fonctionnelles */
772840
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant *toute chose égale par ailleurs*. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ?
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Branchement ||Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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, l'HyperSPARC, les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Opération entière ||Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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|Émission 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. Avec la seconde, les branchements sont traités comme des instructions entières. Le premier cas est illustré ci-dessous.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
* Unité mémoire
| FPU
| Unité de branchement
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
* µops mémoire.
| Opération flottante
| Branchements
|}
La première solution impose la présence d'une unité de branchement séparée, avec son port d'émission. La seconde peut exécuter les branchements soit dans une ALU entière, soit dans une unité de branchement reliée au port d'émission pour les opérations entières. Elle permet d'économiser beaucoup de transistors dans l'unité d'émission/chargement, car on émet une µops en moins. De plus, utiliser l'ALU entière permet d'économiser une unité de calcul.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus atrd. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement''' des unités fonctionnelles, aussi appelé le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé, mais quelques vieux articles académiques l'utilisent. Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Pour ce qui est du gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles. Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duiplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement de l'unité mémoire comme ALU entière===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse. En soi, rien de compliqué. Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
===La duplication des unités fonctionnelles===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'addition. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières. En pratique, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et '''duplication des ALUs'''.
Reste à voir quelles unités dupliquer, et pourquoi. Toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance. Les opérations très fréquemment utilisées gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Cependant, dupliquer les unités mémoire et de branchement n'est pas une mince affaire. En comparaison, dupliquer les ALU entière est trivial.
En conséquence, dupliquer les ALU entières est privilégié sur le reste. Les CPU superscalaires simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, et combinent cela à un partitionnement minimal. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium. Il avait deux ALU entières, un multiplieur, une FPU et une unité mémoire. Il pouvait exécuter les combinaisons INT + INT, INT + MUL, et INT + MEM. Par contre, il ne pouvait pas émettre une instruction flottante avec une autre. En clair, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
Pour les ALU entières, la tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Les seules exceptions sont les CPU superscalaires très larges, qui peuvent se permettre de dupliquer autre chose que les ALU entières. 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. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir exécuter deux multiplications à la fois, idem pour les décalages.
Pour les unités mémoire, nous mettons cela de côté pour le moment. Mais en attendant, nous allons supposer qu'elle n'est pas dupliquée dans les sections qui vont suivre.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
61j1202vms2cklaseif9y1sf3p782f5
772841
772840
2026-09-22T15:48:06Z
Mewtow
31375
/* La duplication des unités fonctionnelles */
772841
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant *toute chose égale par ailleurs*. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ?
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Branchement ||Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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, l'HyperSPARC, les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Opération entière ||Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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|Émission 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. Avec la seconde, les branchements sont traités comme des instructions entières. Le premier cas est illustré ci-dessous.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
* Unité mémoire
| FPU
| Unité de branchement
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
* µops mémoire.
| Opération flottante
| Branchements
|}
La première solution impose la présence d'une unité de branchement séparée, avec son port d'émission. La seconde peut exécuter les branchements soit dans une ALU entière, soit dans une unité de branchement reliée au port d'émission pour les opérations entières. Elle permet d'économiser beaucoup de transistors dans l'unité d'émission/chargement, car on émet une µops en moins. De plus, utiliser l'ALU entière permet d'économiser une unité de calcul.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus atrd. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement''' des unités fonctionnelles, aussi appelé le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé, mais quelques vieux articles académiques l'utilisent. Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Pour ce qui est du gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles. Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duiplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement de l'unité mémoire comme ALU entière===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse. En soi, rien de compliqué. Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
===La duplication des unités fonctionnelles===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'addition. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières. En pratique, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et '''duplication des ALUs'''.
Reste à voir quelles unités dupliquer, et pourquoi. Toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des paires d'instructions fréquentes. Les opérations très fréquemment utilisées gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les unités mémoire et de branchement n'est pas une mince affaire. En comparaison, dupliquer les ALU entière est trivial.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste. Les CPU superscalaires simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, et combinent cela à un partitionnement minimal. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium. Il avait deux ALU entières, un multiplieur, une FPU et une unité mémoire. Il pouvait exécuter les combinaisons INT + INT, INT + MUL, et INT + MEM. Par contre, il ne pouvait pas émettre une instruction flottante avec une autre. En clair, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Les seules exceptions sont les CPU superscalaires très larges, qui peuvent se permettre de dupliquer autre chose que les ALU entières. 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. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir exécuter deux multiplications à la fois, idem pour les décalages.
Les CPU superscalaires larges peuvent se permettre de dupliquer les autres unités, pas seulement les ALU entières. Ils ont tendance à multiplier les unités mémoire, en second, après les ALU entières. Mais nous mettons cela de côté pour le moment, une section entière du chapitre sera dédié aux unités mémoire superscalaires. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
i5kbzd5u690lxtqky7fxz3h4vhlhlsr
772842
772841
2026-09-22T15:48:45Z
Mewtow
31375
/* La duplication des unités fonctionnelles */
772842
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant *toute chose égale par ailleurs*. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ?
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Branchement ||Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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, l'HyperSPARC, les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Opération entière ||Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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|Émission 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. Avec la seconde, les branchements sont traités comme des instructions entières. Le premier cas est illustré ci-dessous.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
* Unité mémoire
| FPU
| Unité de branchement
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
* µops mémoire.
| Opération flottante
| Branchements
|}
La première solution impose la présence d'une unité de branchement séparée, avec son port d'émission. La seconde peut exécuter les branchements soit dans une ALU entière, soit dans une unité de branchement reliée au port d'émission pour les opérations entières. Elle permet d'économiser beaucoup de transistors dans l'unité d'émission/chargement, car on émet une µops en moins. De plus, utiliser l'ALU entière permet d'économiser une unité de calcul.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus atrd. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement''' des unités fonctionnelles, aussi appelé le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé, mais quelques vieux articles académiques l'utilisent. Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Pour ce qui est du gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles. Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duiplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement de l'unité mémoire comme ALU entière===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse. En soi, rien de compliqué. Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
===La duplication des unités fonctionnelles===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'addition. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières. En pratique, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et '''duplication des ALUs'''.
Reste à voir quelles unités dupliquer, et pourquoi. Toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des paires d'instructions fréquentes. Les opérations très fréquemment utilisées gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les unités mémoire et de branchement n'est pas une mince affaire. En comparaison, dupliquer les ALU entière est trivial.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste. Les CPU superscalaires simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, et combinent cela à un partitionnement minimal. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium. Il avait deux ALU entières, un multiplieur, une FPU et une unité mémoire. Il pouvait exécuter les combinaisons INT + INT, INT + MUL, et INT + MEM. Par contre, il ne pouvait pas émettre une instruction flottante avec une autre. En clair, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. 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. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir exécuter deux multiplications à la fois, idem pour les décalages.
Les CPU superscalaires larges peuvent se permettre de dupliquer les autres unités, pas seulement les ALU entières. Ils ont tendance à multiplier les unités mémoire, en second, après les ALU entières. Mais nous mettons cela de côté pour le moment, une section entière du chapitre sera dédié aux unités mémoire superscalaires. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
69acir7xzrpn5pmkg7gd9m396agjpah
772843
772842
2026-09-22T15:48:59Z
Mewtow
31375
/* L'émission multiple des micro-opérations entières */
772843
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant *toute chose égale par ailleurs*. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ?
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Branchement ||Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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, l'HyperSPARC, les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Opération entière ||Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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.
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. Avec la seconde, les branchements sont traités comme des instructions entières. Le premier cas est illustré ci-dessous.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
* Unité mémoire
| FPU
| Unité de branchement
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
* µops mémoire.
| Opération flottante
| Branchements
|}
La première solution impose la présence d'une unité de branchement séparée, avec son port d'émission. La seconde peut exécuter les branchements soit dans une ALU entière, soit dans une unité de branchement reliée au port d'émission pour les opérations entières. Elle permet d'économiser beaucoup de transistors dans l'unité d'émission/chargement, car on émet une µops en moins. De plus, utiliser l'ALU entière permet d'économiser une unité de calcul.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus atrd. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement''' des unités fonctionnelles, aussi appelé le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé, mais quelques vieux articles académiques l'utilisent. Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Pour ce qui est du gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles. Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duiplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement de l'unité mémoire comme ALU entière===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse. En soi, rien de compliqué. Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
===La duplication des unités fonctionnelles===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'addition. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières. En pratique, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et '''duplication des ALUs'''.
Reste à voir quelles unités dupliquer, et pourquoi. Toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des paires d'instructions fréquentes. Les opérations très fréquemment utilisées gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les unités mémoire et de branchement n'est pas une mince affaire. En comparaison, dupliquer les ALU entière est trivial.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste. Les CPU superscalaires simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, et combinent cela à un partitionnement minimal. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium. Il avait deux ALU entières, un multiplieur, une FPU et une unité mémoire. Il pouvait exécuter les combinaisons INT + INT, INT + MUL, et INT + MEM. Par contre, il ne pouvait pas émettre une instruction flottante avec une autre. En clair, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. 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. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir exécuter deux multiplications à la fois, idem pour les décalages.
Les CPU superscalaires larges peuvent se permettre de dupliquer les autres unités, pas seulement les ALU entières. Ils ont tendance à multiplier les unités mémoire, en second, après les ALU entières. Mais nous mettons cela de côté pour le moment, une section entière du chapitre sera dédié aux unités mémoire superscalaires. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
sgsw3007xc5h2yh6b2bhng4y3c0vkyf
772844
772843
2026-09-22T15:49:09Z
Mewtow
31375
/* La duplication des unités fonctionnelles */
772844
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant *toute chose égale par ailleurs*. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ?
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Branchement ||Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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, l'HyperSPARC, les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Opération entière ||Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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.
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. Avec la seconde, les branchements sont traités comme des instructions entières. Le premier cas est illustré ci-dessous.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
* Unité mémoire
| FPU
| Unité de branchement
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
* µops mémoire.
| Opération flottante
| Branchements
|}
La première solution impose la présence d'une unité de branchement séparée, avec son port d'émission. La seconde peut exécuter les branchements soit dans une ALU entière, soit dans une unité de branchement reliée au port d'émission pour les opérations entières. Elle permet d'économiser beaucoup de transistors dans l'unité d'émission/chargement, car on émet une µops en moins. De plus, utiliser l'ALU entière permet d'économiser une unité de calcul.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus atrd. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement''' des unités fonctionnelles, aussi appelé le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé, mais quelques vieux articles académiques l'utilisent. Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Pour ce qui est du gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles. Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duiplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement de l'unité mémoire comme ALU entière===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse. En soi, rien de compliqué. Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
===La duplication des unités fonctionnelles===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'addition. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières. En pratique, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et '''duplication des ALUs'''.
Reste à voir quelles unités dupliquer, et pourquoi. Toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des paires d'instructions fréquentes. Les opérations très fréquemment utilisées gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les unités mémoire et de branchement n'est pas une mince affaire. En comparaison, dupliquer les ALU entière est trivial.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste. Les CPU superscalaires simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, et combinent cela à un partitionnement minimal. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium. Il avait deux ALU entières, un multiplieur, une FPU et une unité mémoire. Il pouvait exécuter les combinaisons INT + INT, INT + MUL, et INT + MEM. Par contre, il ne pouvait pas émettre une instruction flottante avec une autre. En clair, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. 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. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir exécuter deux multiplications à la fois, idem pour les décalages.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Les CPU superscalaires larges peuvent se permettre de dupliquer les autres unités, pas seulement les ALU entières. Ils ont tendance à multiplier les unités mémoire, en second, après les ALU entières. Mais nous mettons cela de côté pour le moment, une section entière du chapitre sera dédié aux unités mémoire superscalaires. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
btdhngo6nok7a1umwyzgibner3cv684
772845
772844
2026-09-22T15:49:44Z
Mewtow
31375
/* La double émission entière-flottante */
772845
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant *toute chose égale par ailleurs*. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ?
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Branchement ||Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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, l'HyperSPARC, les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Opération entière ||Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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.
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. Avec la seconde, les branchements sont traités comme des instructions entières. Le premier cas est illustré ci-dessous.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
* Unité mémoire
| FPU
| Unité de branchement
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
* µops mémoire.
| Opération flottante
| Branchements
|}
La première solution impose la présence d'une unité de branchement séparée, avec son port d'émission. La seconde peut exécuter les branchements soit dans une ALU entière, soit dans une unité de branchement reliée au port d'émission pour les opérations entières. Elle permet d'économiser beaucoup de transistors dans l'unité d'émission/chargement, car on émet une µops en moins. De plus, utiliser l'ALU entière permet d'économiser une unité de calcul.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus atrd. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement''' des unités fonctionnelles, aussi appelé le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé, mais quelques vieux articles académiques l'utilisent. Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Pour ce qui est du gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles. Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duiplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement de l'unité mémoire comme ALU entière===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse. En soi, rien de compliqué. Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
===La duplication des unités fonctionnelles===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'addition. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières. En pratique, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et '''duplication des ALUs'''.
Reste à voir quelles unités dupliquer, et pourquoi. Toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des paires d'instructions fréquentes. Les opérations très fréquemment utilisées gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les unités mémoire et de branchement n'est pas une mince affaire. En comparaison, dupliquer les ALU entière est trivial.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste. Les CPU superscalaires simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, et combinent cela à un partitionnement minimal. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium. Il avait deux ALU entières, un multiplieur, une FPU et une unité mémoire. Il pouvait exécuter les combinaisons INT + INT, INT + MUL, et INT + MEM. Par contre, il ne pouvait pas émettre une instruction flottante avec une autre. En clair, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. 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. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir exécuter deux multiplications à la fois, idem pour les décalages.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Les CPU superscalaires larges peuvent se permettre de dupliquer les autres unités, pas seulement les ALU entières. Ils ont tendance à multiplier les unités mémoire, en second, après les ALU entières. Mais nous mettons cela de côté pour le moment, une section entière du chapitre sera dédié aux unités mémoire superscalaires. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
946l6s2yy591jmzgwi2u4d7frdu9c38
772846
772845
2026-09-22T15:49:56Z
Mewtow
31375
/* L'émission parallèle des branchements */
772846
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant *toute chose égale par ailleurs*. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ?
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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, l'HyperSPARC, les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
| Opération entière || Opération entière ||Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
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.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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.
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. Avec la seconde, les branchements sont traités comme des instructions entières. Le premier cas est illustré ci-dessous.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
* Unité mémoire
| FPU
| Unité de branchement
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
* µops mémoire.
| Opération flottante
| Branchements
|}
La première solution impose la présence d'une unité de branchement séparée, avec son port d'émission. La seconde peut exécuter les branchements soit dans une ALU entière, soit dans une unité de branchement reliée au port d'émission pour les opérations entières. Elle permet d'économiser beaucoup de transistors dans l'unité d'émission/chargement, car on émet une µops en moins. De plus, utiliser l'ALU entière permet d'économiser une unité de calcul.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus atrd. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement''' des unités fonctionnelles, aussi appelé le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé, mais quelques vieux articles académiques l'utilisent. Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Pour ce qui est du gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles. Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duiplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement de l'unité mémoire comme ALU entière===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse. En soi, rien de compliqué. Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
===La duplication des unités fonctionnelles===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'addition. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières. En pratique, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et '''duplication des ALUs'''.
Reste à voir quelles unités dupliquer, et pourquoi. Toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des paires d'instructions fréquentes. Les opérations très fréquemment utilisées gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les unités mémoire et de branchement n'est pas une mince affaire. En comparaison, dupliquer les ALU entière est trivial.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste. Les CPU superscalaires simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, et combinent cela à un partitionnement minimal. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium. Il avait deux ALU entières, un multiplieur, une FPU et une unité mémoire. Il pouvait exécuter les combinaisons INT + INT, INT + MUL, et INT + MEM. Par contre, il ne pouvait pas émettre une instruction flottante avec une autre. En clair, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. 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. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir exécuter deux multiplications à la fois, idem pour les décalages.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Les CPU superscalaires larges peuvent se permettre de dupliquer les autres unités, pas seulement les ALU entières. Ils ont tendance à multiplier les unités mémoire, en second, après les ALU entières. Mais nous mettons cela de côté pour le moment, une section entière du chapitre sera dédié aux unités mémoire superscalaires. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
be9pmi8nkllxlqicynk0xl9d4szx4u5
772847
772846
2026-09-22T15:50:16Z
Mewtow
31375
/* L'émission multiple des micro-opérations entières */
772847
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant *toute chose égale par ailleurs*. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ?
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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, l'HyperSPARC, les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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.
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. Avec la seconde, les branchements sont traités comme des instructions entières. Le premier cas est illustré ci-dessous.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !! Voie d'émission n°4
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
* Unité mémoire
| FPU
| Unité de branchement
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
* µops mémoire.
| Opération flottante
| Branchements
|}
La première solution impose la présence d'une unité de branchement séparée, avec son port d'émission. La seconde peut exécuter les branchements soit dans une ALU entière, soit dans une unité de branchement reliée au port d'émission pour les opérations entières. Elle permet d'économiser beaucoup de transistors dans l'unité d'émission/chargement, car on émet une µops en moins. De plus, utiliser l'ALU entière permet d'économiser une unité de calcul.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus atrd. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement''' des unités fonctionnelles, aussi appelé le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé, mais quelques vieux articles académiques l'utilisent. Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Pour ce qui est du gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles. Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duiplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement de l'unité mémoire comme ALU entière===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse. En soi, rien de compliqué. Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
===La duplication des unités fonctionnelles===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'addition. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières. En pratique, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et '''duplication des ALUs'''.
Reste à voir quelles unités dupliquer, et pourquoi. Toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des paires d'instructions fréquentes. Les opérations très fréquemment utilisées gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les unités mémoire et de branchement n'est pas une mince affaire. En comparaison, dupliquer les ALU entière est trivial.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste. Les CPU superscalaires simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, et combinent cela à un partitionnement minimal. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium. Il avait deux ALU entières, un multiplieur, une FPU et une unité mémoire. Il pouvait exécuter les combinaisons INT + INT, INT + MUL, et INT + MEM. Par contre, il ne pouvait pas émettre une instruction flottante avec une autre. En clair, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. 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. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir exécuter deux multiplications à la fois, idem pour les décalages.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Les CPU superscalaires larges peuvent se permettre de dupliquer les autres unités, pas seulement les ALU entières. Ils ont tendance à multiplier les unités mémoire, en second, après les ALU entières. Mais nous mettons cela de côté pour le moment, une section entière du chapitre sera dédié aux unités mémoire superscalaires. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
137asx3hrnrdr54yho99mpeeklhn9ax
772848
772847
2026-09-22T15:50:59Z
Mewtow
31375
/* L'émission multiple des micro-opérations entières */
772848
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant *toute chose égale par ailleurs*. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ?
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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, l'HyperSPARC, les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
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. 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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.
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. Avec la seconde, les branchements sont traités comme des instructions entières. Le premier cas est illustré ci-dessous.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !! Voie d'émission n°4
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
* Unité mémoire
| FPU
| Unité de branchement
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
* µops mémoire.
| Opération flottante
| Branchements
|}
La première solution impose la présence d'une unité de branchement séparée, avec son port d'émission. La seconde peut exécuter les branchements soit dans une ALU entière, soit dans une unité de branchement reliée au port d'émission pour les opérations entières. Elle permet d'économiser beaucoup de transistors dans l'unité d'émission/chargement, car on émet une µops en moins. De plus, utiliser l'ALU entière permet d'économiser une unité de calcul.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus atrd. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement''' des unités fonctionnelles, aussi appelé le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé, mais quelques vieux articles académiques l'utilisent. Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Pour ce qui est du gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles. Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duiplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement de l'unité mémoire comme ALU entière===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse. En soi, rien de compliqué. Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
===La duplication des unités fonctionnelles===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'addition. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières. En pratique, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et '''duplication des ALUs'''.
Reste à voir quelles unités dupliquer, et pourquoi. Toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des paires d'instructions fréquentes. Les opérations très fréquemment utilisées gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les unités mémoire et de branchement n'est pas une mince affaire. En comparaison, dupliquer les ALU entière est trivial.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste. Les CPU superscalaires simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, et combinent cela à un partitionnement minimal. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium. Il avait deux ALU entières, un multiplieur, une FPU et une unité mémoire. Il pouvait exécuter les combinaisons INT + INT, INT + MUL, et INT + MEM. Par contre, il ne pouvait pas émettre une instruction flottante avec une autre. En clair, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. 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. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir exécuter deux multiplications à la fois, idem pour les décalages.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Les CPU superscalaires larges peuvent se permettre de dupliquer les autres unités, pas seulement les ALU entières. Ils ont tendance à multiplier les unités mémoire, en second, après les ALU entières. Mais nous mettons cela de côté pour le moment, une section entière du chapitre sera dédié aux unités mémoire superscalaires. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
3cflb8qu1s1mst4q4nxv82cr28afrvr
772849
772848
2026-09-22T15:51:36Z
Mewtow
31375
/* Le reconditionnement de l'unité mémoire comme ALU entière */
772849
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant *toute chose égale par ailleurs*. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ?
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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, l'HyperSPARC, les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
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. 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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.
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. Avec la seconde, les branchements sont traités comme des instructions entières. Le premier cas est illustré ci-dessous.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !! Voie d'émission n°4
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
* Unité mémoire
| FPU
| Unité de branchement
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
* µops mémoire.
| Opération flottante
| Branchements
|}
La première solution impose la présence d'une unité de branchement séparée, avec son port d'émission. La seconde peut exécuter les branchements soit dans une ALU entière, soit dans une unité de branchement reliée au port d'émission pour les opérations entières. Elle permet d'économiser beaucoup de transistors dans l'unité d'émission/chargement, car on émet une µops en moins. De plus, utiliser l'ALU entière permet d'économiser une unité de calcul.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus atrd. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement''' des unités fonctionnelles, aussi appelé le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé, mais quelques vieux articles académiques l'utilisent. Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Pour ce qui est du gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles. Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duiplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement de l'unité mémoire comme ALU entière===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. La technique est appelée le '''reconditionnement de l'unité mémoire'''.
L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse. En soi, rien de compliqué. Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
===La duplication des unités fonctionnelles===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'addition. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières. En pratique, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et '''duplication des ALUs'''.
Reste à voir quelles unités dupliquer, et pourquoi. Toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des paires d'instructions fréquentes. Les opérations très fréquemment utilisées gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les unités mémoire et de branchement n'est pas une mince affaire. En comparaison, dupliquer les ALU entière est trivial.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste. Les CPU superscalaires simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, et combinent cela à un partitionnement minimal. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium. Il avait deux ALU entières, un multiplieur, une FPU et une unité mémoire. Il pouvait exécuter les combinaisons INT + INT, INT + MUL, et INT + MEM. Par contre, il ne pouvait pas émettre une instruction flottante avec une autre. En clair, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. 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. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir exécuter deux multiplications à la fois, idem pour les décalages.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Les CPU superscalaires larges peuvent se permettre de dupliquer les autres unités, pas seulement les ALU entières. Ils ont tendance à multiplier les unités mémoire, en second, après les ALU entières. Mais nous mettons cela de côté pour le moment, une section entière du chapitre sera dédié aux unités mémoire superscalaires. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
ii24463ssu3co0cf9vzcy1htgu3upzh
772850
772849
2026-09-22T16:56:51Z
Mewtow
31375
/* La double émission entière-flottante */
772850
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant *toute chose égale par ailleurs*. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU.
Certains processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
D'autres processeurs historiques avaient un circuit multiplier entier, séparé de la FPU. Dans ce cas, les multiplications étaient réalisées dans le pipeline entier. L'émission était de type (INT/MUL) + FLOAT. Le DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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, l'HyperSPARC, les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
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. 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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.
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. Avec la seconde, les branchements sont traités comme des instructions entières. Le premier cas est illustré ci-dessous.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !! Voie d'émission n°4
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
* Unité mémoire
| FPU
| Unité de branchement
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
* µops mémoire.
| Opération flottante
| Branchements
|}
La première solution impose la présence d'une unité de branchement séparée, avec son port d'émission. La seconde peut exécuter les branchements soit dans une ALU entière, soit dans une unité de branchement reliée au port d'émission pour les opérations entières. Elle permet d'économiser beaucoup de transistors dans l'unité d'émission/chargement, car on émet une µops en moins. De plus, utiliser l'ALU entière permet d'économiser une unité de calcul.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus atrd. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement''' des unités fonctionnelles, aussi appelé le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé, mais quelques vieux articles académiques l'utilisent. Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Pour ce qui est du gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles. Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duiplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement de l'unité mémoire comme ALU entière===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. La technique est appelée le '''reconditionnement de l'unité mémoire'''.
L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse. En soi, rien de compliqué. Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
===La duplication des unités fonctionnelles===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'addition. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières. En pratique, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et '''duplication des ALUs'''.
Reste à voir quelles unités dupliquer, et pourquoi. Toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des paires d'instructions fréquentes. Les opérations très fréquemment utilisées gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les unités mémoire et de branchement n'est pas une mince affaire. En comparaison, dupliquer les ALU entière est trivial.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste. Les CPU superscalaires simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, et combinent cela à un partitionnement minimal. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium. Il avait deux ALU entières, un multiplieur, une FPU et une unité mémoire. Il pouvait exécuter les combinaisons INT + INT, INT + MUL, et INT + MEM. Par contre, il ne pouvait pas émettre une instruction flottante avec une autre. En clair, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. 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. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir exécuter deux multiplications à la fois, idem pour les décalages.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Les CPU superscalaires larges peuvent se permettre de dupliquer les autres unités, pas seulement les ALU entières. Ils ont tendance à multiplier les unités mémoire, en second, après les ALU entières. Mais nous mettons cela de côté pour le moment, une section entière du chapitre sera dédié aux unités mémoire superscalaires. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
21ovs7lzpi3b6xnbyz2cve8wsyuqmzk
772851
772850
2026-09-22T16:57:59Z
Mewtow
31375
/* Terminologie pour le reste du chapitre */
772851
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant *toute chose égale par ailleurs*. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU.
Certains processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
D'autres processeurs historiques avaient un circuit multiplier entier, séparé de la FPU. Dans ce cas, les multiplications étaient réalisées dans le pipeline entier. L'émission était de type (INT/MUL) + FLOAT. Le DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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, l'HyperSPARC, les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
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. 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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.
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. Avec la seconde, les branchements sont traités comme des instructions entières. Le premier cas est illustré ci-dessous.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !! Voie d'émission n°4
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
* Unité mémoire
| FPU
| Unité de branchement
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
* µops mémoire.
| Opération flottante
| Branchements
|}
La première solution impose la présence d'une unité de branchement séparée, avec son port d'émission. La seconde peut exécuter les branchements soit dans une ALU entière, soit dans une unité de branchement reliée au port d'émission pour les opérations entières. Elle permet d'économiser beaucoup de transistors dans l'unité d'émission/chargement, car on émet une µops en moins. De plus, utiliser l'ALU entière permet d'économiser une unité de calcul.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus atrd. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement''' des unités fonctionnelles, aussi appelé le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé, mais quelques vieux articles académiques l'utilisent. Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Pour ce qui est du gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles. Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duiplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement de l'unité mémoire comme ALU entière===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. La technique est appelée le '''reconditionnement de l'unité mémoire'''.
L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse. En soi, rien de compliqué. Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
===La duplication des unités fonctionnelles===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'addition. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières. En pratique, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et '''duplication des ALUs'''.
Reste à voir quelles unités dupliquer, et pourquoi. Toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des paires d'instructions fréquentes. Les opérations très fréquemment utilisées gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les unités mémoire et de branchement n'est pas une mince affaire. En comparaison, dupliquer les ALU entière est trivial.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste. Les CPU superscalaires simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, et combinent cela à un partitionnement minimal. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium. Il avait deux ALU entières, un multiplieur, une FPU et une unité mémoire. Il pouvait exécuter les combinaisons INT + INT, INT + MUL, et INT + MEM. Par contre, il ne pouvait pas émettre une instruction flottante avec une autre. En clair, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. 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. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir exécuter deux multiplications à la fois, idem pour les décalages.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Les CPU superscalaires larges peuvent se permettre de dupliquer les autres unités, pas seulement les ALU entières. Ils ont tendance à multiplier les unités mémoire, en second, après les ALU entières. Mais nous mettons cela de côté pour le moment, une section entière du chapitre sera dédié aux unités mémoire superscalaires. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
mo8igkw1fud433cmelwykvpurjy7l5p
772852
772851
2026-09-22T16:59:46Z
Mewtow
31375
/* La double émission entière-flottante */
772852
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant *toute chose égale par ailleurs*. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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, l'HyperSPARC, les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
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. 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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.
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. Avec la seconde, les branchements sont traités comme des instructions entières. Le premier cas est illustré ci-dessous.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !! Voie d'émission n°4
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
* Unité mémoire
| FPU
| Unité de branchement
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
* µops mémoire.
| Opération flottante
| Branchements
|}
La première solution impose la présence d'une unité de branchement séparée, avec son port d'émission. La seconde peut exécuter les branchements soit dans une ALU entière, soit dans une unité de branchement reliée au port d'émission pour les opérations entières. Elle permet d'économiser beaucoup de transistors dans l'unité d'émission/chargement, car on émet une µops en moins. De plus, utiliser l'ALU entière permet d'économiser une unité de calcul.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus atrd. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement''' des unités fonctionnelles, aussi appelé le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé, mais quelques vieux articles académiques l'utilisent. Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Pour ce qui est du gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles. Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duiplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement de l'unité mémoire comme ALU entière===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. La technique est appelée le '''reconditionnement de l'unité mémoire'''.
L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse. En soi, rien de compliqué. Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
===La duplication des unités fonctionnelles===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'addition. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières. En pratique, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et '''duplication des ALUs'''.
Reste à voir quelles unités dupliquer, et pourquoi. Toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des paires d'instructions fréquentes. Les opérations très fréquemment utilisées gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les unités mémoire et de branchement n'est pas une mince affaire. En comparaison, dupliquer les ALU entière est trivial.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste. Les CPU superscalaires simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, et combinent cela à un partitionnement minimal. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium. Il avait deux ALU entières, un multiplieur, une FPU et une unité mémoire. Il pouvait exécuter les combinaisons INT + INT, INT + MUL, et INT + MEM. Par contre, il ne pouvait pas émettre une instruction flottante avec une autre. En clair, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. 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. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir exécuter deux multiplications à la fois, idem pour les décalages.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Les CPU superscalaires larges peuvent se permettre de dupliquer les autres unités, pas seulement les ALU entières. Ils ont tendance à multiplier les unités mémoire, en second, après les ALU entières. Mais nous mettons cela de côté pour le moment, une section entière du chapitre sera dédié aux unités mémoire superscalaires. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
syuoyjdmnqwmsttctcg2b3280x6m8uv
772853
772852
2026-09-22T17:01:16Z
Mewtow
31375
/* La double émission entière-flottante */
772853
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant *toute chose égale par ailleurs*. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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, l'HyperSPARC, les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
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. 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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.
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. Avec la seconde, les branchements sont traités comme des instructions entières. Le premier cas est illustré ci-dessous.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !! Voie d'émission n°4
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
* Unité mémoire
| FPU
| Unité de branchement
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
* µops mémoire.
| Opération flottante
| Branchements
|}
La première solution impose la présence d'une unité de branchement séparée, avec son port d'émission. La seconde peut exécuter les branchements soit dans une ALU entière, soit dans une unité de branchement reliée au port d'émission pour les opérations entières. Elle permet d'économiser beaucoup de transistors dans l'unité d'émission/chargement, car on émet une µops en moins. De plus, utiliser l'ALU entière permet d'économiser une unité de calcul.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus atrd. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement''' des unités fonctionnelles, aussi appelé le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé, mais quelques vieux articles académiques l'utilisent. Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Pour ce qui est du gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles. Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duiplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement de l'unité mémoire comme ALU entière===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. La technique est appelée le '''reconditionnement de l'unité mémoire'''.
L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse. En soi, rien de compliqué. Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
===La duplication des unités fonctionnelles===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'addition. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières. En pratique, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et '''duplication des ALUs'''.
Reste à voir quelles unités dupliquer, et pourquoi. Toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des paires d'instructions fréquentes. Les opérations très fréquemment utilisées gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les unités mémoire et de branchement n'est pas une mince affaire. En comparaison, dupliquer les ALU entière est trivial.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste. Les CPU superscalaires simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, et combinent cela à un partitionnement minimal. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium. Il avait deux ALU entières, un multiplieur, une FPU et une unité mémoire. Il pouvait exécuter les combinaisons INT + INT, INT + MUL, et INT + MEM. Par contre, il ne pouvait pas émettre une instruction flottante avec une autre. En clair, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. 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. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir exécuter deux multiplications à la fois, idem pour les décalages.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Les CPU superscalaires larges peuvent se permettre de dupliquer les autres unités, pas seulement les ALU entières. Ils ont tendance à multiplier les unités mémoire, en second, après les ALU entières. Mais nous mettons cela de côté pour le moment, une section entière du chapitre sera dédié aux unités mémoire superscalaires. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
3t0fh7rklx76nmc4hr8u694jnjy7k1k
772854
772853
2026-09-22T17:19:29Z
Mewtow
31375
772854
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant *toute chose égale par ailleurs*. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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, l'HyperSPARC, les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
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. Avec la seconde, les branchements sont traités comme des instructions entières. Le premier cas est illustré ci-dessous.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !! Voie d'émission n°4
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
* Unité mémoire
| FPU
| Unité de branchement
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
* µops mémoire.
| Opération flottante
| Branchements
|}
La première solution impose la présence d'une unité de branchement séparée, avec son port d'émission. La seconde peut exécuter les branchements soit dans une ALU entière, soit dans une unité de branchement reliée au port d'émission pour les opérations entières. Elle permet d'économiser beaucoup de transistors dans l'unité d'émission/chargement, car on émet une µops en moins. De plus, utiliser l'ALU entière permet d'économiser une unité de calcul.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement''' des unités fonctionnelles, aussi appelé le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé, mais quelques vieux articles académiques l'utilisent. Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Pour ce qui est du gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles. Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duiplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement de l'unité mémoire comme ALU entière===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. La technique est appelée le '''reconditionnement de l'unité mémoire'''.
L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse. En soi, rien de compliqué. Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
===La duplication des unités fonctionnelles===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'addition. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières. En pratique, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et '''duplication des unités'''.
Reste à voir quelles unités dupliquer, et pourquoi. 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, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appairement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appairemment. Les contraintes d'appairemment entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistors plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shfiter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste. Le gain en performance est significatif et le cout en circuit très faible. Les CPU superscalaires simples se contentent d'ailleurs de dupliquer une ALU entière en deux ou trois exemplaires, et combinent cela à un partitionnement minimal.
Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium. Il avait deux ALU entières, un multiplieur, une FPU et une unité mémoire. Il pouvait émettre deux instructions simultannément, dans deux pipeline. Le premier pipleine pouvait faire toutes les opérations, alors que le second ne contenait qu'une ALU entière. Il pouvait donc émettre une instruction entière simultannément avec une autre instruction, avec deux restrictions : les branchements et les opérations flottantes. En clair, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
[[File:Emission multiple des opérations entières, implémentation naive.png|thumb|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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|Émission multiple des opérations entières, implémentation courante]]
Les CPU superscalaires larges peuvent se permettre de dupliquer les autres unités, pas seulement les ALU entières. Ils ont tendance à multiplier les unités mémoire, en second, après les ALU entières. Mais nous mettons cela de côté pour le moment, une section entière du chapitre sera dédié aux unités mémoire superscalaires. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
siwfg9x4tfxdmqxp72kqdj9t43g4ih0
772855
772854
2026-09-22T17:31:36Z
Mewtow
31375
/* La duplication des unités fonctionnelles */
772855
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant *toute chose égale par ailleurs*. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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, l'HyperSPARC, les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
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. Avec la seconde, les branchements sont traités comme des instructions entières. Le premier cas est illustré ci-dessous.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !! Voie d'émission n°4
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
* Unité mémoire
| FPU
| Unité de branchement
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
* µops mémoire.
| Opération flottante
| Branchements
|}
La première solution impose la présence d'une unité de branchement séparée, avec son port d'émission. La seconde peut exécuter les branchements soit dans une ALU entière, soit dans une unité de branchement reliée au port d'émission pour les opérations entières. Elle permet d'économiser beaucoup de transistors dans l'unité d'émission/chargement, car on émet une µops en moins. De plus, utiliser l'ALU entière permet d'économiser une unité de calcul.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement''' des unités fonctionnelles, aussi appelé le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé, mais quelques vieux articles académiques l'utilisent. Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Pour ce qui est du gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles. Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duiplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement de l'unité mémoire comme ALU entière===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. La technique est appelée le '''reconditionnement de l'unité mémoire'''.
L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse. En soi, rien de compliqué. Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. De même, il ne peut pas émettre deux décalages simultanément.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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 voies d'émission séparés, ce qui permet d'émettre 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|Émission multiple des opérations entières, implémentation courante]]
Les CPU superscalaires simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, et combinent cela à un partitionnement minimal. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium. Il avait deux ALU entières, un multiplieur, une FPU et une unité mémoire. Il pouvait émettre deux instructions simultanément, dans deux pipeline. Le premier pipeline pouvait faire toutes les opérations, alors que le second ne contenait qu'une ALU entière. Il pouvait donc émettre une instruction entière simultanément avec une autre instruction. Avec cependant deux restrictions : les branchements et les opérations flottantes. En clair, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter à la troisième place, et les unités de branchement à la quatrième place.
Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Mais nous mettons cela de côté pour le moment, une section entière du chapitre sera dédié aux unités mémoire superscalaires. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités mémoire a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs. la duplication des unités mémoire a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
py9uz098ogrm7l3u4ef5fiw9b9bzcq0
772856
772855
2026-09-22T17:32:19Z
Mewtow
31375
/* La duplication des autres unités fonctionnelles */
772856
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant *toute chose égale par ailleurs*. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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, l'HyperSPARC, les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
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. Avec la seconde, les branchements sont traités comme des instructions entières. Le premier cas est illustré ci-dessous.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !! Voie d'émission n°4
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
* Unité mémoire
| FPU
| Unité de branchement
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
* µops mémoire.
| Opération flottante
| Branchements
|}
La première solution impose la présence d'une unité de branchement séparée, avec son port d'émission. La seconde peut exécuter les branchements soit dans une ALU entière, soit dans une unité de branchement reliée au port d'émission pour les opérations entières. Elle permet d'économiser beaucoup de transistors dans l'unité d'émission/chargement, car on émet une µops en moins. De plus, utiliser l'ALU entière permet d'économiser une unité de calcul.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement''' des unités fonctionnelles, aussi appelé le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé, mais quelques vieux articles académiques l'utilisent. Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Pour ce qui est du gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles. Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duiplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement de l'unité mémoire comme ALU entière===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. La technique est appelée le '''reconditionnement de l'unité mémoire'''.
L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse. En soi, rien de compliqué. Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. De même, il ne peut pas émettre deux décalages simultanément.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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 voies d'émission séparés, ce qui permet d'émettre 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|Émission multiple des opérations entières, implémentation courante]]
Les CPU superscalaires simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, et combinent cela à un partitionnement minimal. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium. Il avait deux ALU entières, un multiplieur, une FPU et une unité mémoire. Il pouvait émettre deux instructions simultanément, dans deux pipeline. Le premier pipeline pouvait faire toutes les opérations, alors que le second ne contenait qu'une ALU entière. Il pouvait donc émettre une instruction entière simultanément avec une autre instruction. Avec cependant deux restrictions : les branchements et les opérations flottantes. En clair, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités mémoire a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs. la duplication des unités mémoire a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
prosy243tmcilacguo4haxq1kgyqs4z
772857
772856
2026-09-22T17:37:26Z
Mewtow
31375
/* Le reconditionnement de l'unité mémoire comme ALU entière */
772857
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant *toute chose égale par ailleurs*. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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, l'HyperSPARC, les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
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. Avec la seconde, les branchements sont traités comme des instructions entières. Le premier cas est illustré ci-dessous.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !! Voie d'émission n°4
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
* Unité mémoire
| FPU
| Unité de branchement
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
* µops mémoire.
| Opération flottante
| Branchements
|}
La première solution impose la présence d'une unité de branchement séparée, avec son port d'émission. La seconde peut exécuter les branchements soit dans une ALU entière, soit dans une unité de branchement reliée au port d'émission pour les opérations entières. Elle permet d'économiser beaucoup de transistors dans l'unité d'émission/chargement, car on émet une µops en moins. De plus, utiliser l'ALU entière permet d'économiser une unité de calcul.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement''' des unités fonctionnelles, aussi appelé le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé, mais quelques vieux articles académiques l'utilisent. Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Pour ce qui est du gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles. Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duiplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unité fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. La technique est appelée le '''reconditionnement de l'unité mémoire'''.
L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse. En soi, rien de compliqué. Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. De même, il ne peut pas émettre deux décalages simultanément.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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 voies d'émission séparés, ce qui permet d'émettre 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|Émission multiple des opérations entières, implémentation courante]]
Les CPU superscalaires simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, et combinent cela à un partitionnement minimal. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium. Il avait deux ALU entières, un multiplieur, une FPU et une unité mémoire. Il pouvait émettre deux instructions simultanément, dans deux pipeline. Le premier pipeline pouvait faire toutes les opérations, alors que le second ne contenait qu'une ALU entière. Il pouvait donc émettre une instruction entière simultanément avec une autre instruction. Avec cependant deux restrictions : les branchements et les opérations flottantes. En clair, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités mémoire a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs. la duplication des unités mémoire a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
7cuoqve2oi8gqlm7ymdahg1tcq11c9e
772858
772857
2026-09-22T17:38:06Z
Mewtow
31375
/* Les CPU superscalaires utilisent toutes ces stratégies */
772858
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant *toute chose égale par ailleurs*. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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, l'HyperSPARC, les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
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. Avec la seconde, les branchements sont traités comme des instructions entières. Le premier cas est illustré ci-dessous.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !! Voie d'émission n°4
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
* Unité mémoire
| FPU
| Unité de branchement
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
* µops mémoire.
| Opération flottante
| Branchements
|}
La première solution impose la présence d'une unité de branchement séparée, avec son port d'émission. La seconde peut exécuter les branchements soit dans une ALU entière, soit dans une unité de branchement reliée au port d'émission pour les opérations entières. Elle permet d'économiser beaucoup de transistors dans l'unité d'émission/chargement, car on émet une µops en moins. De plus, utiliser l'ALU entière permet d'économiser une unité de calcul.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement''' des unités fonctionnelles, aussi appelé le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé, mais quelques vieux articles académiques l'utilisent. Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Pour ce qui est du gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles. Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duiplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unité fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. La technique est appelée le '''reconditionnement de l'unité mémoire'''.
L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse. En soi, rien de compliqué. Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. De même, il ne peut pas émettre deux décalages simultanément.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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 voies d'émission séparés, ce qui permet d'émettre 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|Émission multiple des opérations entières, implémentation courante]]
Les CPU superscalaires simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, et combinent cela à un partitionnement minimal. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium. Il avait deux ALU entières, un multiplieur, une FPU et une unité mémoire. Il pouvait émettre deux instructions simultanément, dans deux pipeline. Le premier pipeline pouvait faire toutes les opérations, alors que le second ne contenait qu'une ALU entière. Il pouvait donc émettre une instruction entière simultanément avec une autre instruction. Avec cependant deux restrictions : les branchements et les opérations flottantes. En clair, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
5wpoloqet0fxz1bquxnyzssax22x21t
772859
772858
2026-09-22T17:38:24Z
Mewtow
31375
/* La duplication des ALU entières */
772859
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant *toute chose égale par ailleurs*. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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, l'HyperSPARC, les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
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. Avec la seconde, les branchements sont traités comme des instructions entières. Le premier cas est illustré ci-dessous.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !! Voie d'émission n°4
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
* Unité mémoire
| FPU
| Unité de branchement
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
* µops mémoire.
| Opération flottante
| Branchements
|}
La première solution impose la présence d'une unité de branchement séparée, avec son port d'émission. La seconde peut exécuter les branchements soit dans une ALU entière, soit dans une unité de branchement reliée au port d'émission pour les opérations entières. Elle permet d'économiser beaucoup de transistors dans l'unité d'émission/chargement, car on émet une µops en moins. De plus, utiliser l'ALU entière permet d'économiser une unité de calcul.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement''' des unités fonctionnelles, aussi appelé le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé, mais quelques vieux articles académiques l'utilisent. Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Pour ce qui est du gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles. Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duiplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unité fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. La technique est appelée le '''reconditionnement de l'unité mémoire'''.
L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse. En soi, rien de compliqué. Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. De même, il ne peut pas émettre deux décalages simultanément.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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 voies d'émission séparés, ce qui permet d'émettre 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|Émission multiple des opérations entières, implémentation courante]]
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
fp4maaleq4k3if06766dz19h7ih0ar8
772860
772859
2026-09-22T17:41:25Z
Mewtow
31375
/* Les CPU superscalaires utilisent toutes ces stratégies */
772860
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant *toute chose égale par ailleurs*. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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, l'HyperSPARC, les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
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. Avec la seconde, les branchements sont traités comme des instructions entières. Le premier cas est illustré ci-dessous.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !! Voie d'émission n°4
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
* Unité mémoire
| FPU
| Unité de branchement
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
* µops mémoire.
| Opération flottante
| Branchements
|}
La première solution impose la présence d'une unité de branchement séparée, avec son port d'émission. La seconde peut exécuter les branchements soit dans une ALU entière, soit dans une unité de branchement reliée au port d'émission pour les opérations entières. Elle permet d'économiser beaucoup de transistors dans l'unité d'émission/chargement, car on émet une µops en moins. De plus, utiliser l'ALU entière permet d'économiser une unité de calcul.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement''' des unités fonctionnelles, aussi appelé le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé, mais quelques vieux articles académiques l'utilisent. Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Pour ce qui est du gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles. Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duiplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unité fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. La technique est appelée le '''reconditionnement de l'unité mémoire'''.
L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse. En soi, rien de compliqué. Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. De même, il ne peut pas émettre deux décalages simultanément.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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 voies d'émission séparés, ce qui permet d'émettre 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|Émission multiple des opérations entières, implémentation courante]]
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, sans utiliser de partitionnement. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium, d'Intel. Il avait dupliqué son ALU entière en deux exemplaires, afin de pouvoir émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : les branchements et les opérations flottantes. Impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. C'était la forme la plus simple de CPU double émission, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
jrtrrb32p4w6y2ra5ikedi9x7r2juqd
772861
772860
2026-09-22T17:48:12Z
Mewtow
31375
/* L'émission multiple des micro-opérations entières */
772861
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant *toute chose égale par ailleurs*. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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, l'HyperSPARC, les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
Pour les branchements, il est possible d'utiliser l'émission parallèle des branchements, ou non. La première solution impose la présence d'une unité de branchement séparée, avec son port d'émission. La seconde peut exécuter les branchements soit dans une ALU entière, soit dans une unité de branchement reliée au port d'émission pour les opérations entières. Elle permet d'économiser beaucoup de transistors dans l'unité d'émission/chargement, car on émet une µops en moins. De plus, utiliser l'ALU entière permet d'économiser une unité de calcul.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
Pour les multiplications, tout dépend de si elles sont exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + MUL sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, la combinaison INT + MUL + FPU est parfaitement possible, mais pas la combinaison INT + INT + MUL.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !! Voie d'émission n°4
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
* Unité mémoire
| FPU
| Unité de branchement
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
* µops mémoire.
| Opération flottante
| Branchements
|}
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement''' des unités fonctionnelles, aussi appelé le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé, mais quelques vieux articles académiques l'utilisent. Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Pour ce qui est du gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles. Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duiplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unité fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. La technique est appelée le '''reconditionnement de l'unité mémoire'''.
L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse. En soi, rien de compliqué. Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. De même, il ne peut pas émettre deux décalages simultanément.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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 voies d'émission séparés, ce qui permet d'émettre 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|Émission multiple des opérations entières, implémentation courante]]
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, sans utiliser de partitionnement. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium, d'Intel. Il avait dupliqué son ALU entière en deux exemplaires, afin de pouvoir émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : les branchements et les opérations flottantes. Impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. C'était la forme la plus simple de CPU double émission, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
3fcdmy4teo9q5pvrp64g62lyk4c1hai
772862
772861
2026-09-22T17:49:27Z
Mewtow
31375
/* L'émission multiple des micro-opérations entières */
772862
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant *toute chose égale par ailleurs*. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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, l'HyperSPARC, les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
Pour les branchements, il est possible d'utiliser l'émission parallèle des branchements, ou non. La première solution impose la présence d'une unité de branchement séparée, avec son port d'émission. La seconde peut exécuter les branchements soit dans une ALU entière, soit dans une unité de branchement reliée au port d'émission pour les opérations entières. Elle permet d'économiser beaucoup de transistors dans l'unité d'émission/chargement, car on émet une µops en moins. De plus, utiliser l'ALU entière permet d'économiser une unité de calcul.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
Pour les multiplications, tout dépend de si elles sont exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + MUL sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, la combinaison INT + MUL + FPU est parfaitement possible, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !! Voie d'émission n°4
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
* Unité mémoire
| FPU
| Unité de branchement
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
* µops mémoire.
| Opération flottante
| Branchements
|}
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement''' des unités fonctionnelles, aussi appelé le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé, mais quelques vieux articles académiques l'utilisent. Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Pour ce qui est du gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles. Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duiplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unité fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. La technique est appelée le '''reconditionnement de l'unité mémoire'''.
L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse. En soi, rien de compliqué. Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. De même, il ne peut pas émettre deux décalages simultanément.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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 voies d'émission séparés, ce qui permet d'émettre 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|Émission multiple des opérations entières, implémentation courante]]
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, sans utiliser de partitionnement. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium, d'Intel. Il avait dupliqué son ALU entière en deux exemplaires, afin de pouvoir émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : les branchements et les opérations flottantes. Impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. C'était la forme la plus simple de CPU double émission, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
10t87ee2psf4frfqqqwre20ftc7gzwi
772863
772862
2026-09-22T18:05:11Z
Mewtow
31375
/* L'émission multiple des micro-opérations entières */
772863
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant *toute chose égale par ailleurs*. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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, l'HyperSPARC, les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
Pour les branchements, il est possible de les exécuter dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions. Une autre possibilité utilise l'émission parallèle des branchements, ce qui impose la présence d'une unité de branchement séparée.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
Pour les multiplications, tout dépend de si elles sont exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + MUL sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, la combinaison INT + MUL + FPU est parfaitement possible, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !! Voie d'émission n°4
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
* Unité mémoire
| FPU
| Unité de branchement
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
* µops mémoire.
| Opération flottante
| Branchements
|}
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement''' des unités fonctionnelles, aussi appelé le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé, mais quelques vieux articles académiques l'utilisent. Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Pour ce qui est du gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles. Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duiplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unité fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. La technique est appelée le '''reconditionnement de l'unité mémoire'''.
L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse. En soi, rien de compliqué. Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. De même, il ne peut pas émettre deux décalages simultanément.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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 voies d'émission séparés, ce qui permet d'émettre 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|Émission multiple des opérations entières, implémentation courante]]
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, sans utiliser de partitionnement. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium, d'Intel. Il avait dupliqué son ALU entière en deux exemplaires, afin de pouvoir émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : les branchements et les opérations flottantes. Impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. C'était la forme la plus simple de CPU double émission, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
5mjg4fuur9f7aukvw41d57gijtj4cjf
772864
772863
2026-09-22T18:06:08Z
Mewtow
31375
/* L'émission multiple des micro-opérations entières */
772864
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant *toute chose égale par ailleurs*. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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, l'HyperSPARC, les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
Pour les multiplications, tout dépend de si elles sont exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + MUL sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, la combinaison INT + MUL + FPU est parfaitement possible, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il est possible de les exécuter dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions. Une autre possibilité utilise l'émission parallèle des branchements, ce qui impose la présence d'une unité de branchement séparée.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement''' des unités fonctionnelles, aussi appelé le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé, mais quelques vieux articles académiques l'utilisent. Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Pour ce qui est du gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles. Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duiplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unité fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. La technique est appelée le '''reconditionnement de l'unité mémoire'''.
L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse. En soi, rien de compliqué. Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. De même, il ne peut pas émettre deux décalages simultanément.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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 voies d'émission séparés, ce qui permet d'émettre 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|Émission multiple des opérations entières, implémentation courante]]
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, sans utiliser de partitionnement. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium, d'Intel. Il avait dupliqué son ALU entière en deux exemplaires, afin de pouvoir émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : les branchements et les opérations flottantes. Impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. C'était la forme la plus simple de CPU double émission, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
c8i29vdwzpzg93l2esezglonnv5nuc7
772865
772864
2026-09-22T18:07:22Z
Mewtow
31375
/* La duplication des ALU entières */
772865
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant *toute chose égale par ailleurs*. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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, l'HyperSPARC, les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
Pour les multiplications, tout dépend de si elles sont exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + MUL sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, la combinaison INT + MUL + FPU est parfaitement possible, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il est possible de les exécuter dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions. Une autre possibilité utilise l'émission parallèle des branchements, ce qui impose la présence d'une unité de branchement séparée.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement''' des unités fonctionnelles, aussi appelé le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé, mais quelques vieux articles académiques l'utilisent. Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Pour ce qui est du gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles. Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duiplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unité fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. La technique est appelée le '''reconditionnement de l'unité mémoire'''.
L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse. En soi, rien de compliqué. Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
===L'émission entière multiple : la duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. De même, il ne peut pas émettre deux décalages simultanément.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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 voies d'émission séparés, ce qui permet d'émettre 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|Émission multiple des opérations entières, implémentation courante]]
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, sans utiliser de partitionnement. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium, d'Intel. Il avait dupliqué son ALU entière en deux exemplaires, afin de pouvoir émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : les branchements et les opérations flottantes. Impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. C'était la forme la plus simple de CPU double émission, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
tnwi9a3ithltogp5neo7snmreix0azl
772866
772865
2026-09-22T18:07:34Z
Mewtow
31375
/* L'émission entière multiple : la duplication des ALU entières */
772866
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant *toute chose égale par ailleurs*. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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, l'HyperSPARC, les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
Pour les multiplications, tout dépend de si elles sont exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + MUL sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, la combinaison INT + MUL + FPU est parfaitement possible, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il est possible de les exécuter dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions. Une autre possibilité utilise l'émission parallèle des branchements, ce qui impose la présence d'une unité de branchement séparée.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement''' des unités fonctionnelles, aussi appelé le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé, mais quelques vieux articles académiques l'utilisent. Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Pour ce qui est du gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles. Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duiplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unité fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. La technique est appelée le '''reconditionnement de l'unité mémoire'''.
L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse. En soi, rien de compliqué. Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. De même, il ne peut pas émettre deux décalages simultanément.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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 voies d'émission séparés, ce qui permet d'émettre 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|Émission multiple des opérations entières, implémentation courante]]
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, sans utiliser de partitionnement. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium, d'Intel. Il avait dupliqué son ALU entière en deux exemplaires, afin de pouvoir émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : les branchements et les opérations flottantes. Impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. C'était la forme la plus simple de CPU double émission, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
flnqgjij3ov6rpwu7huhafhz7om3mpf
772867
772866
2026-09-22T18:08:59Z
Mewtow
31375
/* Le reconditionnement des unité fonctionnelles */
772867
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant *toute chose égale par ailleurs*. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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, l'HyperSPARC, les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
Pour les multiplications, tout dépend de si elles sont exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + MUL sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, la combinaison INT + MUL + FPU est parfaitement possible, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il est possible de les exécuter dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions. Une autre possibilité utilise l'émission parallèle des branchements, ce qui impose la présence d'une unité de branchement séparée.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement''' des unités fonctionnelles, aussi appelé le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé, mais quelques vieux articles académiques l'utilisent. Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Pour ce qui est du gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles. Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duiplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unité fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
En soi, rien de compliqué. Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. De même, il ne peut pas émettre deux décalages simultanément.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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 voies d'émission séparés, ce qui permet d'émettre 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|Émission multiple des opérations entières, implémentation courante]]
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, sans utiliser de partitionnement. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium, d'Intel. Il avait dupliqué son ALU entière en deux exemplaires, afin de pouvoir émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : les branchements et les opérations flottantes. Impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. C'était la forme la plus simple de CPU double émission, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
cjnznf4a119gqnmxst1qlsgibswpbqg
772868
772867
2026-09-22T18:10:52Z
Mewtow
31375
/* Le reconditionnement des unité fonctionnelles */
772868
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant *toute chose égale par ailleurs*. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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, l'HyperSPARC, les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
Pour les multiplications, tout dépend de si elles sont exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + MUL sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, la combinaison INT + MUL + FPU est parfaitement possible, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il est possible de les exécuter dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions. Une autre possibilité utilise l'émission parallèle des branchements, ce qui impose la présence d'une unité de branchement séparée.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement''' des unités fonctionnelles, aussi appelé le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé, mais quelques vieux articles académiques l'utilisent. Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Pour ce qui est du gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles. Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duiplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unité fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. De même, il ne peut pas émettre deux décalages simultanément.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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 voies d'émission séparés, ce qui permet d'émettre 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|Émission multiple des opérations entières, implémentation courante]]
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, sans utiliser de partitionnement. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium, d'Intel. Il avait dupliqué son ALU entière en deux exemplaires, afin de pouvoir émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : les branchements et les opérations flottantes. Impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. C'était la forme la plus simple de CPU double émission, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
90xmz9mb4b3hqfp8jdbckdyv8xwg4ot
772869
772868
2026-09-22T18:11:02Z
Mewtow
31375
/* Le reconditionnement des unité fonctionnelles */
772869
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant *toute chose égale par ailleurs*. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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, l'HyperSPARC, les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
Pour les multiplications, tout dépend de si elles sont exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + MUL sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, la combinaison INT + MUL + FPU est parfaitement possible, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il est possible de les exécuter dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions. Une autre possibilité utilise l'émission parallèle des branchements, ce qui impose la présence d'une unité de branchement séparée.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement''' des unités fonctionnelles, aussi appelé le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé, mais quelques vieux articles académiques l'utilisent. Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Pour ce qui est du gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles. Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duiplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. De même, il ne peut pas émettre deux décalages simultanément.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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 voies d'émission séparés, ce qui permet d'émettre 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|Émission multiple des opérations entières, implémentation courante]]
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, sans utiliser de partitionnement. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium, d'Intel. Il avait dupliqué son ALU entière en deux exemplaires, afin de pouvoir émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : les branchements et les opérations flottantes. Impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. C'était la forme la plus simple de CPU double émission, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
7dzd79dacptmdequtd1gemf8q9f007p
772870
772869
2026-09-22T18:14:35Z
Mewtow
31375
/* Le nombre de voies d'un processeur superscalaire */
772870
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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, l'HyperSPARC, les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
Pour les multiplications, tout dépend de si elles sont exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + MUL sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, la combinaison INT + MUL + FPU est parfaitement possible, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il est possible de les exécuter dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions. Une autre possibilité utilise l'émission parallèle des branchements, ce qui impose la présence d'une unité de branchement séparée.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement''' des unités fonctionnelles, aussi appelé le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé, mais quelques vieux articles académiques l'utilisent. Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Pour ce qui est du gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles. Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duiplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. De même, il ne peut pas émettre deux décalages simultanément.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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 voies d'émission séparés, ce qui permet d'émettre 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|Émission multiple des opérations entières, implémentation courante]]
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, sans utiliser de partitionnement. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium, d'Intel. Il avait dupliqué son ALU entière en deux exemplaires, afin de pouvoir émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : les branchements et les opérations flottantes. Impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. C'était la forme la plus simple de CPU double émission, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
qwe62vxbwxozf9aulugzmpldjowxhxz
772871
772870
2026-09-22T18:17:46Z
Mewtow
31375
/* L'émission multiple des micro-opérations entières */
772871
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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, l'HyperSPARC, les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
Pour les multiplications, tout dépend de si elles sont exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il est possible de les exécuter dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions. Une autre possibilité utilise l'émission parallèle des branchements, ce qui impose la présence d'une unité de branchement séparée.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement''' des unités fonctionnelles, aussi appelé le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé, mais quelques vieux articles académiques l'utilisent. Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Pour ce qui est du gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles. Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duiplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. De même, il ne peut pas émettre deux décalages simultanément.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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 voies d'émission séparés, ce qui permet d'émettre 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|Émission multiple des opérations entières, implémentation courante]]
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, sans utiliser de partitionnement. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium, d'Intel. Il avait dupliqué son ALU entière en deux exemplaires, afin de pouvoir émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : les branchements et les opérations flottantes. Impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. C'était la forme la plus simple de CPU double émission, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
9ap0w0m3h9dx0vspk6elhh34cji36e5
772872
772871
2026-09-22T18:20:51Z
Mewtow
31375
/* Le partitionnement des unités fonctionnelles */
772872
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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, l'HyperSPARC, les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
Pour les multiplications, tout dépend de si elles sont exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il est possible de les exécuter dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions. Une autre possibilité utilise l'émission parallèle des branchements, ce qui impose la présence d'une unité de branchement séparée.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité. Le gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles. Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duiplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. De même, il ne peut pas émettre deux décalages simultanément.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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 voies d'émission séparés, ce qui permet d'émettre 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|Émission multiple des opérations entières, implémentation courante]]
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, sans utiliser de partitionnement. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium, d'Intel. Il avait dupliqué son ALU entière en deux exemplaires, afin de pouvoir émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : les branchements et les opérations flottantes. Impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. C'était la forme la plus simple de CPU double émission, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
74bk6ts8ki0w1xnnzdtfkxji9qcs93o
772873
772872
2026-09-22T18:37:47Z
Mewtow
31375
/* Les CPU superscalaires utilisent toutes ces stratégies */
772873
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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, l'HyperSPARC, les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
Pour les multiplications, tout dépend de si elles sont exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il est possible de les exécuter dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions. Une autre possibilité utilise l'émission parallèle des branchements, ce qui impose la présence d'une unité de branchement séparée.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité. Le gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles. Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duiplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. De même, il ne peut pas émettre deux décalages simultanément.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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 voies d'émission séparés, ce qui permet d'émettre 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|Émission multiple des opérations entières, implémentation courante]]
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, sans utiliser de partitionnement. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium, d'Intel. Il avait dupliqué son ALU entière en deux exemplaires, afin de pouvoir émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : les branchements et les opérations flottantes. Impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. C'était la forme la plus simple de CPU double émission, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
D'autres processeurs superscalaires plus complexes utilisent duplication et partitionnement. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, une FPU, deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement de ce processeur sont très simples : tant qu'il y a assez d'unités pour exécuter deux µops, la paire est permise. Il est donc possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
sns628ter4kgsts20pykckrfh4t74td
772874
772873
2026-09-22T18:43:16Z
Mewtow
31375
/* Les CPU superscalaires utilisent toutes ces stratégies */
772874
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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, l'HyperSPARC, les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
Pour les multiplications, tout dépend de si elles sont exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il est possible de les exécuter dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions. Une autre possibilité utilise l'émission parallèle des branchements, ce qui impose la présence d'une unité de branchement séparée.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité. Le gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles. Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duiplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. De même, il ne peut pas émettre deux décalages simultanément.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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 voies d'émission séparés, ce qui permet d'émettre 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|Émission multiple des opérations entières, implémentation courante]]
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, sans utiliser de partitionnement. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium, d'Intel. Il avait dupliqué son ALU entière en deux exemplaires, afin de pouvoir émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : les branchements et les opérations flottantes. Impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. C'était la forme la plus simple de CPU double émission, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
Les processeurs superscalaires plus complexes utilisent duplication et partitionnement.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d’appariement plus strictes sont possibles, mais passons.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT/MUL + FLOAT, INT/MUL + MEM, FLOAT + MEM.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
c3hg3t19n41yx9wd4cnpesi2que38rn
772875
772874
2026-09-22T18:46:06Z
Mewtow
31375
/* L'unité d'émission d'un processeur superscalaire */
772875
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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, l'HyperSPARC, les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
Pour les multiplications, tout dépend de si elles sont exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il est possible de les exécuter dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions. Une autre possibilité utilise l'émission parallèle des branchements, ce qui impose la présence d'une unité de branchement séparée.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité. Le gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles. Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duiplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. De même, il ne peut pas émettre deux décalages simultanément.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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 voies d'émission séparés, ce qui permet d'émettre 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|Émission multiple des opérations entières, implémentation courante]]
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, sans utiliser de partitionnement. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium, d'Intel. Il avait dupliqué son ALU entière en deux exemplaires, afin de pouvoir émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : les branchements et les opérations flottantes. Impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. C'était la forme la plus simple de CPU double émission, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
Les processeurs superscalaires plus complexes utilisent duplication et partitionnement.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
18xcss14yrxilff60fpamhnfrf5hg1f
772876
772875
2026-09-22T18:47:12Z
Mewtow
31375
/* L'émission parallèle des branchements */
772876
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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 les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
Pour les multiplications, tout dépend de si elles sont exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il est possible de les exécuter dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions. Une autre possibilité utilise l'émission parallèle des branchements, ce qui impose la présence d'une unité de branchement séparée.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent deux techniques : le partitionnement, et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité. Le gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles. Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duiplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. De même, il ne peut pas émettre deux décalages simultanément.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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 voies d'émission séparés, ce qui permet d'émettre 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|Émission multiple des opérations entières, implémentation courante]]
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, sans utiliser de partitionnement. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium, d'Intel. Il avait dupliqué son ALU entière en deux exemplaires, afin de pouvoir émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : les branchements et les opérations flottantes. Impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. C'était la forme la plus simple de CPU double émission, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
Les processeurs superscalaires plus complexes utilisent duplication et partitionnement.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
jgw9phwknik529rdaqu097gckjo3y4z
772877
772876
2026-09-22T18:52:28Z
Mewtow
31375
/* Les unités de calcul d'un processeur superscalaire */
772877
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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 les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
Pour les multiplications, tout dépend de si elles sont exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il est possible de les exécuter dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions. Une autre possibilité utilise l'émission parallèle des branchements, ce qui impose la présence d'une unité de branchement séparée.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité. Le gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet possible d'implémenter la double émission en utilisant les unités de calcul usuelles. Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite dde ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duiplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. De même, il ne peut pas émettre deux décalages simultanément.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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 voies d'émission séparés, ce qui permet d'émettre 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|Émission multiple des opérations entières, implémentation courante]]
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, sans utiliser de partitionnement. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium, d'Intel. Il avait dupliqué son ALU entière en deux exemplaires, afin de pouvoir émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : les branchements et les opérations flottantes. Impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. C'était la forme la plus simple de CPU double émission, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
Les processeurs superscalaires plus complexes utilisent duplication et partitionnement.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
4z710zbhsibl3jpnawf888qmoyu8s3a
772878
772877
2026-09-22T18:55:32Z
Mewtow
31375
/* Le partitionnement des unités fonctionnelles */
772878
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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 les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des opérations complexes comme les multiplications et les divisions ?
Pour les multiplications, tout dépend de si elles sont exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il est possible de les exécuter dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions. Une autre possibilité utilise l'émission parallèle des branchements, ce qui impose la présence d'une unité de branchement séparée.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité. Le gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duiplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. De même, il ne peut pas émettre deux décalages simultanément.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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 voies d'émission séparés, ce qui permet d'émettre 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|Émission multiple des opérations entières, implémentation courante]]
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, sans utiliser de partitionnement. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium, d'Intel. Il avait dupliqué son ALU entière en deux exemplaires, afin de pouvoir émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : les branchements et les opérations flottantes. Impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. C'était la forme la plus simple de CPU double émission, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
Les processeurs superscalaires plus complexes utilisent duplication et partitionnement.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
6j0jm22y1zsa5y8botlyu8ihfssnofs
772879
772878
2026-09-22T18:58:46Z
Mewtow
31375
/* L'émission multiple des micro-opérations entières */
772879
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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 les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il est possible de les exécuter dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions. Une autre possibilité utilise l'émission parallèle des branchements, ce qui impose la présence d'une unité de branchement séparée.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité. Le gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duiplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. De même, il ne peut pas émettre deux décalages simultanément.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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 voies d'émission séparés, ce qui permet d'émettre 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|Émission multiple des opérations entières, implémentation courante]]
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, sans utiliser de partitionnement. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium, d'Intel. Il avait dupliqué son ALU entière en deux exemplaires, afin de pouvoir émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : les branchements et les opérations flottantes. Impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. C'était la forme la plus simple de CPU double émission, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
Les processeurs superscalaires plus complexes utilisent duplication et partitionnement.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
qgydzh27sk5x7m4tgu72u7y0nysarj8
772880
772879
2026-09-22T18:59:51Z
Mewtow
31375
/* L'émission multiple des micro-opérations entières */
772880
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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 les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il est possible de les exécuter dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
Une autre possibilité utilise l'émission parallèle des branchements, ce qui impose la présence d'une unité de branchement séparée.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité. Le gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duiplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. De même, il ne peut pas émettre deux décalages simultanément.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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 voies d'émission séparés, ce qui permet d'émettre 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|Émission multiple des opérations entières, implémentation courante]]
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, sans utiliser de partitionnement. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium, d'Intel. Il avait dupliqué son ALU entière en deux exemplaires, afin de pouvoir émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : les branchements et les opérations flottantes. Impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. C'était la forme la plus simple de CPU double émission, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
Les processeurs superscalaires plus complexes utilisent duplication et partitionnement.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
f0zt9d3hyxof4lxc7whehkfq18n2qpk
772881
772880
2026-09-22T19:10:56Z
Mewtow
31375
/* Le partitionnement intra-unité */
772881
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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 les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il est possible de les exécuter dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
Une autre possibilité utilise l'émission parallèle des branchements, ce qui impose la présence d'une unité de branchement séparée.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité. Le gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. De même, il ne peut pas émettre deux décalages simultanément.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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 voies d'émission séparés, ce qui permet d'émettre 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|Émission multiple des opérations entières, implémentation courante]]
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, sans utiliser de partitionnement. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium, d'Intel. Il avait dupliqué son ALU entière en deux exemplaires, afin de pouvoir émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : les branchements et les opérations flottantes. Impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. C'était la forme la plus simple de CPU double émission, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
Les processeurs superscalaires plus complexes utilisent duplication et partitionnement.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
58kilyu5f1oqa99cqokdbhpcpdidh0n
772882
772881
2026-09-22T19:11:12Z
Mewtow
31375
/* La duplication des ALU entières */
772882
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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 les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il est possible de les exécuter dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
Une autre possibilité utilise l'émission parallèle des branchements, ce qui impose la présence d'une unité de branchement séparée.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité. Le gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. De même, il ne peut pas émettre deux décalages simultanément.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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 voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, sans utiliser de partitionnement. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium, d'Intel. Il avait dupliqué son ALU entière en deux exemplaires, afin de pouvoir émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : les branchements et les opérations flottantes. Impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. C'était la forme la plus simple de CPU double émission, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
Les processeurs superscalaires plus complexes utilisent duplication et partitionnement.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante.
Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
oqbpnlvfz5blobwgoh1sh62m9h4uq5d
772883
772882
2026-09-22T19:12:32Z
Mewtow
31375
/* L'unité d'émission d'un processeur superscalaire */
772883
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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 les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il est possible de les exécuter dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
Une autre possibilité utilise l'émission parallèle des branchements, ce qui impose la présence d'une unité de branchement séparée.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité. Le gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. De même, il ne peut pas émettre deux décalages simultanément.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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 voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, sans utiliser de partitionnement. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium, d'Intel. Il avait dupliqué son ALU entière en deux exemplaires, afin de pouvoir émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : les branchements et les opérations flottantes. Impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. C'était la forme la plus simple de CPU double émission, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
Les processeurs superscalaires plus complexes utilisent duplication et partitionnement.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
p0uujvrg45fcuma07nj6dzn9svd72z0
772884
772883
2026-09-22T19:13:10Z
Mewtow
31375
/* Les CPU superscalaires utilisent toutes ces stratégies */
772884
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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 les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il est possible de les exécuter dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
Une autre possibilité utilise l'émission parallèle des branchements, ce qui impose la présence d'une unité de branchement séparée.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité. Le gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. De même, il ne peut pas émettre deux décalages simultanément.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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 voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, sans utiliser de partitionnement. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium, d'Intel. Il avait dupliqué son ALU entière en deux exemplaires, afin de pouvoir émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : les branchements et les opérations flottantes. Impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. C'était la forme la plus simple de CPU double émission, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
bazehlbrc5xmlyjy739ghu4y833tgot
772885
772884
2026-09-22T19:17:30Z
Mewtow
31375
/* L'unité d'émission d'un processeur superscalaire */
772885
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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 les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il est possible de les exécuter dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
Une autre possibilité utilise l'émission parallèle des branchements, ce qui impose la présence d'une unité de branchement séparée.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==Les unités de calcul d'un processeur superscalaire==
Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité. Le gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. De même, il ne peut pas émettre deux décalages simultanément.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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 voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, sans utiliser de partitionnement. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium, d'Intel. Il avait dupliqué son ALU entière en deux exemplaires, afin de pouvoir émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : les branchements et les opérations flottantes. Impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. C'était la forme la plus simple de CPU double émission, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
rug0kwnkfm1hsob84r722kgiaahlodj
772886
772885
2026-09-22T19:20:52Z
Mewtow
31375
/* Les unités de calcul d'un processeur superscalaire */
772886
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour le reste du chapitre===
Afin de simplifier les explications de ce chapitre, je vais utiliser les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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 les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il est possible de les exécuter dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
Une autre possibilité utilise l'émission parallèle des branchements, ce qui impose la présence d'une unité de branchement séparée.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité. Le gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. De même, il ne peut pas émettre deux décalages simultanément.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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 voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, sans utiliser de partitionnement. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium, d'Intel. Il avait dupliqué son ALU entière en deux exemplaires, afin de pouvoir émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : les branchements et les opérations flottantes. Impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. C'était la forme la plus simple de CPU double émission, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
08elcbodydf0b7nudsle2z92w1v0jp5
772887
772886
2026-09-22T19:25:16Z
Mewtow
31375
/* Le nombre de voies d'un processeur superscalaire */
772887
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Nous allons commencer par voir les CPU superscalaires historiques, qui ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Elles ont été relâchées par la suite. Les CPU historiques ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
Il y avait une certaine continuité dans les CPU historiques, avec une amélioration constante. Mais après, l'évolution a revu de fond en comble ces fondations et les nouveaux designs n'avaient plus rien en commun. Après les années 90, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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 les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il est possible de les exécuter dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
Une autre possibilité utilise l'émission parallèle des branchements, ce qui impose la présence d'une unité de branchement séparée.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité. Le gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. De même, il ne peut pas émettre deux décalages simultanément.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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 voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, sans utiliser de partitionnement. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium, d'Intel. Il avait dupliqué son ALU entière en deux exemplaires, afin de pouvoir émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : les branchements et les opérations flottantes. Impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. C'était la forme la plus simple de CPU double émission, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
aubvznolw2fytvdcatwqrwgb4vpgiy4
772888
772887
2026-09-22T19:28:30Z
Mewtow
31375
/* L'évolution historique des processeurs superscalaires */
772888
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. La séparation entre les deux a eu lieu approximativement dans les années 90. Durant cette décennie, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. En conséquence, ils ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques. Il y avait une certaine continuité dans les CPU historiques, qui ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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 les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il est possible de les exécuter dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
Une autre possibilité utilise l'émission parallèle des branchements, ce qui impose la présence d'une unité de branchement séparée.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité. Le gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. De même, il ne peut pas émettre deux décalages simultanément.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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 voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, sans utiliser de partitionnement. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium, d'Intel. Il avait dupliqué son ALU entière en deux exemplaires, afin de pouvoir émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : les branchements et les opérations flottantes. Impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. C'était la forme la plus simple de CPU double émission, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
848uq9k0z19oxrq85cf81lnm2ddo0zz
772889
772888
2026-09-22T19:31:27Z
Mewtow
31375
/* L'émission parallèle des branchements */
772889
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. La séparation entre les deux a eu lieu approximativement dans les années 90. Durant cette décennie, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. En conséquence, ils ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques. Il y avait une certaine continuité dans les CPU historiques, qui ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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 les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, reliée au registre d'état et au séquenceur. 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 utilise 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il est possible de les exécuter dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
Une autre possibilité utilise l'émission parallèle des branchements, ce qui impose la présence d'une unité de branchement séparée.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité. Le gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. De même, il ne peut pas émettre deux décalages simultanément.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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 voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, sans utiliser de partitionnement. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium, d'Intel. Il avait dupliqué son ALU entière en deux exemplaires, afin de pouvoir émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : les branchements et les opérations flottantes. Impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. C'était la forme la plus simple de CPU double émission, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
iru4ort9yo1qwzdrtfur0e4wkpwbgdo
772890
772889
2026-09-22T19:31:45Z
Mewtow
31375
/* L'émission parallèle des branchements */
772890
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. La séparation entre les deux a eu lieu approximativement dans les années 90. Durant cette décennie, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. En conséquence, ils ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques. Il y avait une certaine continuité dans les CPU historiques, qui ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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 les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, reliée au registre d'état et au séquenceur. 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 utilise 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 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il est possible de les exécuter dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
Une autre possibilité utilise l'émission parallèle des branchements, ce qui impose la présence d'une unité de branchement séparée.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité. Le gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. De même, il ne peut pas émettre deux décalages simultanément.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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 voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, sans utiliser de partitionnement. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium, d'Intel. Il avait dupliqué son ALU entière en deux exemplaires, afin de pouvoir émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : les branchements et les opérations flottantes. Impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. C'était la forme la plus simple de CPU double émission, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
rxlec7y19ei31y1an083tkft2d52r8h
772891
772890
2026-09-22T19:32:24Z
Mewtow
31375
/* L'émission multiple des micro-opérations entières */
772891
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. La séparation entre les deux a eu lieu approximativement dans les années 90. Durant cette décennie, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. En conséquence, ils ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques. Il y avait une certaine continuité dans les CPU historiques, qui ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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 les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, reliée au registre d'état et au séquenceur. 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 utilise 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 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===L'émission multiple des micro-opérations entières===
Le troisième type de CPU superscalaire historique 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. Les 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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Dans la suite, 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il est possible de les exécuter dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
Une autre possibilité utilise l'émission parallèle des branchements, ce qui impose la présence d'une unité de branchement séparée.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité. Le gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. De même, il ne peut pas émettre deux décalages simultanément.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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 voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, sans utiliser de partitionnement. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium, d'Intel. Il avait dupliqué son ALU entière en deux exemplaires, afin de pouvoir émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : les branchements et les opérations flottantes. Impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. C'était la forme la plus simple de CPU double émission, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
bijhlig7f5swyff4dgq8505bsq3n4ni
772892
772891
2026-09-22T19:33:12Z
Mewtow
31375
/* L'émission multiple des micro-opérations entières */
772892
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. La séparation entre les deux a eu lieu approximativement dans les années 90. Durant cette décennie, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. En conséquence, ils ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques. Il y avait une certaine continuité dans les CPU historiques, qui ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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 les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, reliée au registre d'état et au séquenceur. 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 utilise 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 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===L'émission multiple des micro-opérations entières===
Le troisième type de CPU superscalaire historique 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. Les 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il est possible de les exécuter dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
Une autre possibilité utilise l'émission parallèle des branchements, ce qui impose la présence d'une unité de branchement séparée.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité. Le gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. De même, il ne peut pas émettre deux décalages simultanément.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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 voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, sans utiliser de partitionnement. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium, d'Intel. Il avait dupliqué son ALU entière en deux exemplaires, afin de pouvoir émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : les branchements et les opérations flottantes. Impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. C'était la forme la plus simple de CPU double émission, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
scrbh6v29ibyp2phbztyc2u8r7gtcxq
772893
772892
2026-09-22T19:34:26Z
Mewtow
31375
/* L'émission multiple des micro-opérations entières */
772893
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. La séparation entre les deux a eu lieu approximativement dans les années 90. Durant cette décennie, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. En conséquence, ils ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques. Il y avait une certaine continuité dans les CPU historiques, qui ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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 les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, reliée au registre d'état et au séquenceur. 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 utilise 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 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===L'émission multiple des micro-opérations entières===
Le troisième type de CPU superscalaire historique 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. Les 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité. Le gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. De même, il ne peut pas émettre deux décalages simultanément.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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 voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, sans utiliser de partitionnement. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium, d'Intel. Il avait dupliqué son ALU entière en deux exemplaires, afin de pouvoir émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : les branchements et les opérations flottantes. Impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. C'était la forme la plus simple de CPU double émission, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
8b2utuc1li6v8fernt2ne9nw9hvyefp
772894
772893
2026-09-22T19:36:08Z
Mewtow
31375
/* Le reconditionnement des unités fonctionnelles */
772894
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. La séparation entre les deux a eu lieu approximativement dans les années 90. Durant cette décennie, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. En conséquence, ils ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques. Il y avait une certaine continuité dans les CPU historiques, qui ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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 les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, reliée au registre d'état et au séquenceur. 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 utilise 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 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===L'émission multiple des micro-opérations entières===
Le troisième type de CPU superscalaire historique 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. Les 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité. Le gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. De même, il ne peut pas émettre deux décalages simultanément.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
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 voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, sans utiliser de partitionnement. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium, d'Intel. Il avait dupliqué son ALU entière en deux exemplaires, afin de pouvoir émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : les branchements et les opérations flottantes. Impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. C'était la forme la plus simple de CPU double émission, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
65uygyvjdjracwwnqvxsrb90ue17sok
772895
772894
2026-09-22T19:36:44Z
Mewtow
31375
/* La duplication des ALU entières */
772895
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. La séparation entre les deux a eu lieu approximativement dans les années 90. Durant cette décennie, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. En conséquence, ils ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques. Il y avait une certaine continuité dans les CPU historiques, qui ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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 les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, reliée au registre d'état et au séquenceur. 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 utilise 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 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===L'émission multiple des micro-opérations entières===
Le troisième type de CPU superscalaire historique 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. Les 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité. Le gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. De même, il ne peut pas émettre deux décalages simultanément.
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 voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
| ALU entière
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
| Additions, soustractions, opérations bit à bit, etc.
| Opération flottante
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, sans utiliser de partitionnement. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium, d'Intel. Il avait dupliqué son ALU entière en deux exemplaires, afin de pouvoir émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : les branchements et les opérations flottantes. Impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. C'était la forme la plus simple de CPU double émission, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
2clblfdw041hbv5eaki8ik14lz9ajt6
772896
772895
2026-09-22T19:37:50Z
Mewtow
31375
/* La duplication des ALU entières */
772896
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. La séparation entre les deux a eu lieu approximativement dans les années 90. Durant cette décennie, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. En conséquence, ils ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques. Il y avait une certaine continuité dans les CPU historiques, qui ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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 les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, reliée au registre d'état et au séquenceur. 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 utilise 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 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===L'émission multiple des micro-opérations entières===
Le troisième type de CPU superscalaire historique 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. Les 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité. Le gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Sauf sur les CPU superscalaires très larges, qui peuvent se le permettre. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. De même, il ne peut pas émettre deux décalages simultanément.
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 voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, sans utiliser de partitionnement. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium, d'Intel. Il avait dupliqué son ALU entière en deux exemplaires, afin de pouvoir émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : les branchements et les opérations flottantes. Impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. C'était la forme la plus simple de CPU double émission, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
n26ofrhg4lpz6fs4crz97qpacidgj2x
772897
772896
2026-09-22T19:39:43Z
Mewtow
31375
/* La duplication des ALU entières */
772897
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. La séparation entre les deux a eu lieu approximativement dans les années 90. Durant cette décennie, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. En conséquence, ils ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques. Il y avait une certaine continuité dans les CPU historiques, qui ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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 les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, reliée au registre d'état et au séquenceur. 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 utilise 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 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===L'émission multiple des micro-opérations entières===
Le troisième type de CPU superscalaire historique 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. Les 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité. Le gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur, ni le ''barrel shifter''. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
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 voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, sans utiliser de partitionnement. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium, d'Intel. Il avait dupliqué son ALU entière en deux exemplaires, afin de pouvoir émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : les branchements et les opérations flottantes. Impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. C'était la forme la plus simple de CPU double émission, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
q46q6evm6gptmlioawjcsyxyt3467b5
772898
772897
2026-09-22T19:40:23Z
Mewtow
31375
/* La duplication des ALU entières */
772898
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. La séparation entre les deux a eu lieu approximativement dans les années 90. Durant cette décennie, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. En conséquence, ils ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques. Il y avait une certaine continuité dans les CPU historiques, qui ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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 les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, reliée au registre d'état et au séquenceur. 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 utilise 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 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===L'émission multiple des micro-opérations entières===
Le troisième type de CPU superscalaire historique 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. Les 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité. Le gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui 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. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, sans utiliser de partitionnement. Pour donner un exemple, le premier CPU superscalaire grand public était le Pentium, d'Intel. Il avait dupliqué son ALU entière en deux exemplaires, afin de pouvoir émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : les branchements et les opérations flottantes. Impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. C'était la forme la plus simple de CPU double émission, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
5zg6nsbimdu4s5w421qic5o9bwjzh3v
772899
772898
2026-09-22T19:44:35Z
Mewtow
31375
/* Les CPU superscalaires utilisent toutes ces stratégies */
772899
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. La séparation entre les deux a eu lieu approximativement dans les années 90. Durant cette décennie, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. En conséquence, ils ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques. Il y avait une certaine continuité dans les CPU historiques, qui ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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 les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, reliée au registre d'état et au séquenceur. 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 utilise 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 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===L'émission multiple des micro-opérations entières===
Le troisième type de CPU superscalaire historique 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. Les 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité. Le gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est en effet d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui 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. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, sans utiliser de partitionnement. Pour donner un exemple, le premier CPU superscalaire grand public était l'Intel Pentium. Il dupliquait son ALU entière en deux exemplaires, afin de pouvoir émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : les branchements et les opérations flottantes. Impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. C'était la forme la plus simple de CPU double émission, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
opeok4o87gjy1pqts166cue71xmnv9a
772900
772899
2026-09-22T19:46:00Z
Mewtow
31375
/* Le partitionnement des unités fonctionnelles */
772900
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. La séparation entre les deux a eu lieu approximativement dans les années 90. Durant cette décennie, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. En conséquence, ils ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques. Il y avait une certaine continuité dans les CPU historiques, qui ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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 les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, reliée au registre d'état et au séquenceur. 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 utilise 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 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===L'émission multiple des micro-opérations entières===
Le troisième type de CPU superscalaire historique 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. Les 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité. Le gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est ppossible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui 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. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière en deux ou trois exemplaires, sans utiliser de partitionnement. Pour donner un exemple, le premier CPU superscalaire grand public était l'Intel Pentium. Il dupliquait son ALU entière en deux exemplaires, afin de pouvoir émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : les branchements et les opérations flottantes. Impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. C'était la forme la plus simple de CPU double émission, rien à voir avec une sorte d'évolution des designs historiques. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. Mais pour les CPU non-x86, les choses n'étaient pas différentes.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
g5gxgz5t7qo3amw6xsmweolf0pip7zc
772901
772900
2026-09-22T19:48:31Z
Mewtow
31375
/* Les CPU superscalaires utilisent toutes ces stratégies */
772901
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. La séparation entre les deux a eu lieu approximativement dans les années 90. Durant cette décennie, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. En conséquence, ils ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques. Il y avait une certaine continuité dans les CPU historiques, qui ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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 les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, reliée au registre d'état et au séquenceur. 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 utilise 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 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===L'émission multiple des micro-opérations entières===
Le troisième type de CPU superscalaire historique 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. Les 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité. Le gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est ppossible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui 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. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
ra1nite52el84ihe1qccw6gvy5y3nii
772902
772901
2026-09-22T20:08:44Z
Mewtow
31375
/* Les CPU superscalaires utilisent toutes ces stratégies */
772902
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. La séparation entre les deux a eu lieu approximativement dans les années 90. Durant cette décennie, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. En conséquence, ils ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques. Il y avait une certaine continuité dans les CPU historiques, qui ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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 les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, reliée au registre d'état et au séquenceur. 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 utilise 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 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===L'émission multiple des micro-opérations entières===
Le troisième type de CPU superscalaire historique 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. Les 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Il permet de rendre un CPU superscalaire, avec un cout en circuit très limité, et pour un gain en performance assez limité. Le gain en performance, il est surtout limité par les programmes à exécuter. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est ppossible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui 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. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques l'utilisaient, quelques CPU des années 70-80, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
oafn78t8ai4x4lj5cek6az2nvksiwm3
772903
772902
2026-09-22T20:11:02Z
Mewtow
31375
/* Le partitionnement des unités fonctionnelles */
772903
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. La séparation entre les deux a eu lieu approximativement dans les années 90. Durant cette décennie, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. En conséquence, ils ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques. Il y avait une certaine continuité dans les CPU historiques, qui ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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 les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, reliée au registre d'état et au séquenceur. 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 utilise 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 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===L'émission multiple des micro-opérations entières===
Le troisième type de CPU superscalaire historique 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. Les 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est ppossible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui 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. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques l'utilisaient, quelques CPU des années 70-80, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire le nombre d'instructions chargées/décodées. Dans l’exemple précédent, il est possible d'utiliser la quadruple émission. Le CPU peut alors émettre 3 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 3 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 3 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément une addition, un accès mémoire et une opération flottante.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
doxro1rbfss2x70o2i3859gtie5b1cp
772904
772903
2026-09-22T20:13:40Z
Mewtow
31375
/* L'unité d'émission d'un processeur superscalaire */
772904
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. La séparation entre les deux a eu lieu approximativement dans les années 90. Durant cette décennie, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. En conséquence, ils ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques. Il y avait une certaine continuité dans les CPU historiques, qui ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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 les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, reliée au registre d'état et au séquenceur. 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 utilise 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 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===L'émission multiple des micro-opérations entières===
Le troisième type de CPU superscalaire historique 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. Les 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est ppossible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui 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. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques l'utilisaient, quelques CPU des années 70-80, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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. Voyons pourquoi.
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors émettre 4 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 4 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
'''
Une 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
0394k9llmi7p0ngrqzu8a9hu1274qvr
772905
772904
2026-09-22T20:16:02Z
Mewtow
31375
/* L'implémentation des processeurs superscalaires */
772905
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. La séparation entre les deux a eu lieu approximativement dans les années 90. Durant cette décennie, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. En conséquence, ils ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques. Il y avait une certaine continuité dans les CPU historiques, qui ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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 les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, reliée au registre d'état et au séquenceur. 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 utilise 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 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===L'émission multiple des micro-opérations entières===
Le troisième type de CPU superscalaire historique 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. Les 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est ppossible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui 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. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques l'utilisaient, quelques CPU des années 70-80, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==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. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors émettre 4 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 4 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
===La combinaison des deux techniques précédentes===
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
==Le ''front-end'' 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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
Une 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 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
qp1vid3d1j9nj12omjta5pu8ssrvyy7
772906
772905
2026-09-22T20:18:10Z
Mewtow
31375
/* L'unité de renommage superscalaire */
772906
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. La séparation entre les deux a eu lieu approximativement dans les années 90. Durant cette décennie, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. En conséquence, ils ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques. Il y avait une certaine continuité dans les CPU historiques, qui ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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 les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, reliée au registre d'état et au séquenceur. 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 utilise 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 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===L'émission multiple des micro-opérations entières===
Le troisième type de CPU superscalaire historique 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. Les 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est ppossible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui 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. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques l'utilisaient, quelques CPU des années 70-80, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==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. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors émettre 4 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 4 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
===La combinaison des deux techniques précédentes===
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
==Le ''front-end'' 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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
Une 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 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 superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
kcf4f8n7xyn1eacjyq29awhkrjdvj04
772907
772906
2026-09-22T20:19:00Z
Mewtow
31375
772907
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. La séparation entre les deux a eu lieu approximativement dans les années 90. Durant cette décennie, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. En conséquence, ils ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques. Il y avait une certaine continuité dans les CPU historiques, qui ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===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. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle d'une instruction entière. 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 les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601''). 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. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, reliée au registre d'état et au séquenceur. 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 utilise 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 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===L'émission multiple des micro-opérations entières===
Le troisième type de CPU superscalaire historique 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. Les 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est ppossible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui 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. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques l'utilisaient, quelques CPU des années 70-80, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==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. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors émettre 4 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 4 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
===La combinaison des deux techniques précédentes===
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
==Le ''front-end'' 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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
Une 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 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 superscalaires 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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
b3su28l6o1wuretlf1nlzs6e8vpz9gf
772908
772907
2026-09-22T20:27:26Z
Mewtow
31375
/* L'émission parallèle des branchements */
772908
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. La séparation entre les deux a eu lieu approximativement dans les années 90. Durant cette décennie, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. En conséquence, ils ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques. Il y avait une certaine continuité dans les CPU historiques, qui ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===L'émission parallèle des branchements===
La double émission entière-flottante était l'implémentation la plus simple qui soit. L'étape suivante visait elle aussi à exploiter les unités de calcul déjà présentes dans le processeur. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle des autres instructions. Combinée à la double émission entière-flottante, cela permettait d'émettre en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les branchements étant fréquents sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601'').
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, reliée au registre d'état et au séquenceur. 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 utilise 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 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===L'émission multiple des micro-opérations entières===
Le troisième type de CPU superscalaire historique 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. Les 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est ppossible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui 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. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques l'utilisaient, quelques CPU des années 70-80, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==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. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ?
Une solution pour cela est alors de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors émettre 4 instructions simultanées, qui seront réparties sur les ports d'émission adéquats. Le CPU a alors 4 ports de décodage et 6 ports d'émission. Une implémentation assez complexe permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
===La combinaison des deux techniques précédentes===
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
==Le ''front-end'' 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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
Une 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 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 superscalaires 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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
1dopa65hrj451d5fi4o2mot3r5g3az6
772909
772908
2026-09-22T20:40:25Z
Mewtow
31375
/* L'excès de ports d'émission */
772909
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. La séparation entre les deux a eu lieu approximativement dans les années 90. Durant cette décennie, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. En conséquence, ils ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques. Il y avait une certaine continuité dans les CPU historiques, qui ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===L'émission parallèle des branchements===
La double émission entière-flottante était l'implémentation la plus simple qui soit. L'étape suivante visait elle aussi à exploiter les unités de calcul déjà présentes dans le processeur. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle des autres instructions. Combinée à la double émission entière-flottante, cela permettait d'émettre en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les branchements étant fréquents sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601'').
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, reliée au registre d'état et au séquenceur. 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 utilise 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 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===L'émission multiple des micro-opérations entières===
Le troisième type de CPU superscalaire historique 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. Les 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est ppossible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui 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. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques l'utilisaient, quelques CPU des années 70-80, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==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. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle est assez imparfaite. L'inconvénient est que les ports d'émission sont sous-utilisés. Mais les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir de connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés chacun à une ALU entière, et rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites, ce qui n'est pas le cas avec l'usage de six ports d'émission.
Pour le dire autrement, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, à l'origine de nouvelles contraintes d’appariement. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Mais on peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
===La combinaison des deux techniques précédentes===
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
==Le ''front-end'' 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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
Une 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 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 superscalaires 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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
5gg99f12d1vd86z3z4k3yw9dy9otkgp
772910
772909
2026-09-22T20:42:47Z
Mewtow
31375
/* Le partage des ports d'émission */
772910
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. La séparation entre les deux a eu lieu approximativement dans les années 90. Durant cette décennie, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. En conséquence, ils ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques. Il y avait une certaine continuité dans les CPU historiques, qui ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===L'émission parallèle des branchements===
La double émission entière-flottante était l'implémentation la plus simple qui soit. L'étape suivante visait elle aussi à exploiter les unités de calcul déjà présentes dans le processeur. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle des autres instructions. Combinée à la double émission entière-flottante, cela permettait d'émettre en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les branchements étant fréquents sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601'').
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, reliée au registre d'état et au séquenceur. 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 utilise 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 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===L'émission multiple des micro-opérations entières===
Le troisième type de CPU superscalaire historique 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. Les 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est ppossible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui 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. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques l'utilisaient, quelques CPU des années 70-80, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==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. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
===La combinaison des deux techniques précédentes===
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
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 micro-architecture 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.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
==Le ''front-end'' 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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
Une 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 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 superscalaires 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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
hmg6y966fv4wwceilgipjypfuu0xczu
772911
772910
2026-09-22T20:46:33Z
Mewtow
31375
/* La combinaison des deux techniques précédentes */
772911
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. La séparation entre les deux a eu lieu approximativement dans les années 90. Durant cette décennie, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. En conséquence, ils ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques. Il y avait une certaine continuité dans les CPU historiques, qui ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===L'émission parallèle des branchements===
La double émission entière-flottante était l'implémentation la plus simple qui soit. L'étape suivante visait elle aussi à exploiter les unités de calcul déjà présentes dans le processeur. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle des autres instructions. Combinée à la double émission entière-flottante, cela permettait d'émettre en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les branchements étant fréquents sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601'').
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, reliée au registre d'état et au séquenceur. 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 utilise 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 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===L'émission multiple des micro-opérations entières===
Le troisième type de CPU superscalaire historique 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. Les 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est ppossible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui 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. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques l'utilisaient, quelques CPU des années 70-80, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==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. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
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 à l'émission", et détecte les dépendances entre avec les µops en vol, 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 vol. Mais avec la siperscalarité, elle doit en plus 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 et les sérialiser si besoin. 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 n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, 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 micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. 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-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' 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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
Une 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 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 superscalaires 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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
gdpc0bu3ttpoy8jndgnrivut2u0e3h3
772912
772911
2026-09-22T20:46:45Z
Mewtow
31375
/* Le front-end des processeurs superscalaires */
772912
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. La séparation entre les deux a eu lieu approximativement dans les années 90. Durant cette décennie, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. En conséquence, ils ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques. Il y avait une certaine continuité dans les CPU historiques, qui ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===L'émission parallèle des branchements===
La double émission entière-flottante était l'implémentation la plus simple qui soit. L'étape suivante visait elle aussi à exploiter les unités de calcul déjà présentes dans le processeur. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle des autres instructions. Combinée à la double émission entière-flottante, cela permettait d'émettre en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les branchements étant fréquents sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601'').
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, reliée au registre d'état et au séquenceur. 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 utilise 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 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===L'émission multiple des micro-opérations entières===
Le troisième type de CPU superscalaire historique 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. Les 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est ppossible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui 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. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques l'utilisaient, quelques CPU des années 70-80, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==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. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
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 à l'émission", et détecte les dépendances entre avec les µops en vol, 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 vol. Mais avec la siperscalarité, elle doit en plus 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 et les sérialiser si besoin. 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 n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, 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 micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. 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-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
Une 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 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 superscalaires 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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
knefpljklnm1qbk06o1c2ayjdjr9dms
772913
772912
2026-09-22T20:47:03Z
Mewtow
31375
/* L'intérieur de l'unité d'émission */
772913
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. La séparation entre les deux a eu lieu approximativement dans les années 90. Durant cette décennie, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. En conséquence, ils ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques. Il y avait une certaine continuité dans les CPU historiques, qui ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===L'émission parallèle des branchements===
La double émission entière-flottante était l'implémentation la plus simple qui soit. L'étape suivante visait elle aussi à exploiter les unités de calcul déjà présentes dans le processeur. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle des autres instructions. Combinée à la double émission entière-flottante, cela permettait d'émettre en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les branchements étant fréquents sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601'').
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, reliée au registre d'état et au séquenceur. 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 utilise 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 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===L'émission multiple des micro-opérations entières===
Le troisième type de CPU superscalaire historique 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. Les 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est ppossible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui 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. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques l'utilisaient, quelques CPU des années 70-80, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==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. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
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 à l'émission", et détecte les dépendances entre avec les µops en vol, 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 vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
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.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. 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 n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, 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 micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. 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-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=1.5|Émission multiple des µops mémoire.]]
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
Une 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 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 superscalaires 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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
3pdtzogivh7uhpn064i5rdlx9wcwqmr
772914
772913
2026-09-22T20:48:02Z
Mewtow
31375
/* L'émission multiple des accès mémoire avec une LSQ */
772914
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. La séparation entre les deux a eu lieu approximativement dans les années 90. Durant cette décennie, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. En conséquence, ils ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques. Il y avait une certaine continuité dans les CPU historiques, qui ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===L'émission parallèle des branchements===
La double émission entière-flottante était l'implémentation la plus simple qui soit. L'étape suivante visait elle aussi à exploiter les unités de calcul déjà présentes dans le processeur. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle des autres instructions. Combinée à la double émission entière-flottante, cela permettait d'émettre en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les branchements étant fréquents sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601'').
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, reliée au registre d'état et au séquenceur. 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 utilise 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 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===L'émission multiple des micro-opérations entières===
Le troisième type de CPU superscalaire historique 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. Les 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est ppossible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui 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. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques l'utilisaient, quelques CPU des années 70-80, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==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. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
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 à l'émission", et détecte les dépendances entre avec les µops en vol, 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 vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
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.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. 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 n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, 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 micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. 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-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
[[File:Emission LOAD - Store séparée.png|centre|vignette||upright=1.5|Émission LOAD - Store séparée]]
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
Une 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 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 superscalaires 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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
o007hfq7kmn706fyqpzsuqw8ot2lr1m
772915
772914
2026-09-22T20:48:29Z
Mewtow
31375
/* L'émission séparée des LOAD et STORE */
772915
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. La séparation entre les deux a eu lieu approximativement dans les années 90. Durant cette décennie, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. En conséquence, ils ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques. Il y avait une certaine continuité dans les CPU historiques, qui ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===L'émission parallèle des branchements===
La double émission entière-flottante était l'implémentation la plus simple qui soit. L'étape suivante visait elle aussi à exploiter les unités de calcul déjà présentes dans le processeur. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle des autres instructions. Combinée à la double émission entière-flottante, cela permettait d'émettre en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les branchements étant fréquents sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601'').
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, reliée au registre d'état et au séquenceur. 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 utilise 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 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===L'émission multiple des micro-opérations entières===
Le troisième type de CPU superscalaire historique 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. Les 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est ppossible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui 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. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques l'utilisaient, quelques CPU des années 70-80, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==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. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
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 à l'émission", et détecte les dépendances entre avec les µops en vol, 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 vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
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.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. 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 n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, 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 micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. 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-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===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===
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. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
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.
Une 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 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 superscalaires 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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
cw50fntov9lon5rr0vxnbigorl3x3v8
772916
772915
2026-09-22T20:49:28Z
Mewtow
31375
/* Le banc de registre d'un processeur superscalaire */
772916
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. La séparation entre les deux a eu lieu approximativement dans les années 90. Durant cette décennie, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. En conséquence, ils ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques. Il y avait une certaine continuité dans les CPU historiques, qui ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===L'émission parallèle des branchements===
La double émission entière-flottante était l'implémentation la plus simple qui soit. L'étape suivante visait elle aussi à exploiter les unités de calcul déjà présentes dans le processeur. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle des autres instructions. Combinée à la double émission entière-flottante, cela permettait d'émettre en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les branchements étant fréquents sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601'').
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, reliée au registre d'état et au séquenceur. 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 utilise 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 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===L'émission multiple des micro-opérations entières===
Le troisième type de CPU superscalaire historique 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. Les 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est ppossible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui 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. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques l'utilisaient, quelques CPU des années 70-80, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==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. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
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 à l'émission", et détecte les dépendances entre avec les µops en vol, 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 vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
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.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. 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 n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, 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 micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. 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-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===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===
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. En général, 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.
Une 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 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 superscalaires 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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
27wzn1cnvilq6x6ttmp4ceixrvjuktv
772917
772916
2026-09-22T20:54:00Z
Mewtow
31375
/* La macro-fusion : une optimisation du décodage parallèle */
772917
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. La séparation entre les deux a eu lieu approximativement dans les années 90. Durant cette décennie, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. En conséquence, ils ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques. Il y avait une certaine continuité dans les CPU historiques, qui ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===L'émission parallèle des branchements===
La double émission entière-flottante était l'implémentation la plus simple qui soit. L'étape suivante visait elle aussi à exploiter les unités de calcul déjà présentes dans le processeur. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle des autres instructions. Combinée à la double émission entière-flottante, cela permettait d'émettre en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les branchements étant fréquents sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601'').
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, reliée au registre d'état et au séquenceur. 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 utilise 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 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===L'émission multiple des micro-opérations entières===
Le troisième type de CPU superscalaire historique 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. Les 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est ppossible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui 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. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques l'utilisaient, quelques CPU des années 70-80, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==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. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
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 à l'émission", et détecte les dépendances entre avec les µops en vol, 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 vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
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.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. 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 n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, 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 micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. 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-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===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===
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. En général, 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.
Une 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 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 superscalaires 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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour 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. Elle 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 une unité 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
pjw6jkjqazwlfgywu42ca56gw20uhe0
772918
772917
2026-09-22T21:05:28Z
Mewtow
31375
/* La macro-fusion : une optimisation du décodage parallèle */
772918
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. La séparation entre les deux a eu lieu approximativement dans les années 90. Durant cette décennie, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. En conséquence, ils ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques. Il y avait une certaine continuité dans les CPU historiques, qui ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===L'émission parallèle des branchements===
La double émission entière-flottante était l'implémentation la plus simple qui soit. L'étape suivante visait elle aussi à exploiter les unités de calcul déjà présentes dans le processeur. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle des autres instructions. Combinée à la double émission entière-flottante, cela permettait d'émettre en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les branchements étant fréquents sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601'').
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, reliée au registre d'état et au séquenceur. 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 utilise 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 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===L'émission multiple des micro-opérations entières===
Le troisième type de CPU superscalaire historique 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. Les 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est ppossible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui 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. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques l'utilisaient, quelques CPU des années 70-80, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==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. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
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 à l'émission", et détecte les dépendances entre avec les µops en vol, 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 vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
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.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. 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 n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, 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 micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. 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-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===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===
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. En général, 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.
Une 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 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 superscalaires 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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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 macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Une seconde utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour 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. Elle 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 une unité 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
hrkgfen23xn9j0o7tyq8vpduq502spo
772919
772918
2026-09-22T21:17:55Z
Mewtow
31375
/* La macro-fusion : une optimisation du décodage parallèle */
772919
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. La séparation entre les deux a eu lieu approximativement dans les années 90. Durant cette décennie, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. En conséquence, ils ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques. Il y avait une certaine continuité dans les CPU historiques, qui ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===L'émission parallèle des branchements===
La double émission entière-flottante était l'implémentation la plus simple qui soit. L'étape suivante visait elle aussi à exploiter les unités de calcul déjà présentes dans le processeur. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle des autres instructions. Combinée à la double émission entière-flottante, cela permettait d'émettre en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les branchements étant fréquents sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601'').
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, reliée au registre d'état et au séquenceur. 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 utilise 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 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===L'émission multiple des micro-opérations entières===
Le troisième type de CPU superscalaire historique 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. Les 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est ppossible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui 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. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques l'utilisaient, quelques CPU des années 70-80, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==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. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
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 à l'émission", et détecte les dépendances entre avec les µops en vol, 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 vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
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.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. 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 n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, 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 micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. 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-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===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===
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. En général, 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.
Une 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 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 superscalaires 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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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 macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour 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. Elle 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 une unité 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
3vn5zxss8xlzjcxlwn1x1tu0a1t29xm
772920
772919
2026-09-22T22:16:52Z
Mewtow
31375
/* Le chargement de plusieurs instructions consécutives */
772920
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. La séparation entre les deux a eu lieu approximativement dans les années 90. Durant cette décennie, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. En conséquence, ils ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques. Il y avait une certaine continuité dans les CPU historiques, qui ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===L'émission parallèle des branchements===
La double émission entière-flottante était l'implémentation la plus simple qui soit. L'étape suivante visait elle aussi à exploiter les unités de calcul déjà présentes dans le processeur. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle des autres instructions. Combinée à la double émission entière-flottante, cela permettait d'émettre en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les branchements étant fréquents sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601'').
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, reliée au registre d'état et au séquenceur. 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 utilise 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 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===L'émission multiple des micro-opérations entières===
Le troisième type de CPU superscalaire historique 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. Les 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est ppossible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui 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. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques l'utilisaient, quelques CPU des années 70-80, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==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. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
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 à l'émission", et détecte les dépendances entre avec les µops en vol, 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 vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
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.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. 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 n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, 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 micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. 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-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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.
Un détail est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 1228 bits. Si ce n'est pas le cas, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Dans ce cas, on charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Charger plusieurs instructions consécutives 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 ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement 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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===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===
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. En général, 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.
Une 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 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 superscalaires 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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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 macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour 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. Elle 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 une unité 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
pq3z7nq0hct1y5rbbi9xourw1t3s39h
772921
772920
2026-09-22T22:35:03Z
Mewtow
31375
/* Le chargement de plusieurs instructions consécutives */
772921
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. La séparation entre les deux a eu lieu approximativement dans les années 90. Durant cette décennie, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. En conséquence, ils ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques. Il y avait une certaine continuité dans les CPU historiques, qui ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===L'émission parallèle des branchements===
La double émission entière-flottante était l'implémentation la plus simple qui soit. L'étape suivante visait elle aussi à exploiter les unités de calcul déjà présentes dans le processeur. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle des autres instructions. Combinée à la double émission entière-flottante, cela permettait d'émettre en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les branchements étant fréquents sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601'').
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, reliée au registre d'état et au séquenceur. 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 utilise 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 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===L'émission multiple des micro-opérations entières===
Le troisième type de CPU superscalaire historique 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. Les 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est ppossible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui 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. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques l'utilisaient, quelques CPU des années 70-80, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==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. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
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 à l'émission", et détecte les dépendances entre avec les µops en vol, 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 vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
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.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. 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 n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, 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 micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. 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-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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.
Charger plusieurs instructions consécutives 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 ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement 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.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Mais certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instruction même s'il n'est pas aligné. Le cache d'instruction de ce processeur est un cache à deux voies, où une ligne de cache d'instruction contient exactement 16 instructions. Chaque voie est composée de 4 SRAM sont numérotées 1, 2, 3 et 4. 4 instructions consécutives sont placées dans 4 SRAMs, avec une instruction par SRAM. Le chargement d'un bloc aligné demande juste de lire les 4 SRAMs en parallèle et de concaténer le résultat.
Le chargement d'un bloc aligné demande lui de faire quelques trucs en plus. Imaginons qu'on charge 4 instructions non-alignées, la première étant en troisième place d'un bloc. La première instruction est donc dans la SRAM 3, le suivant dans la 4, puis on revient à la SRAM 1 puis 2. Il est donc possible de charger les quatre instructions en même temps. Par contre, pour les deux dernières instructions, lues dans la SRAM 1 et 2, on doit lire les instructions qui sont une adresse après ! On veut lire les adresses après le bloc, pas avant. Des circuits détectent cette situation en regardant les bits de poids faible de l'adresse et l'incrémentent si besoin.
Notez que cette technique marche si les instructions sont alignées sur une ligne de cache. Si une instruction est à cheval sur deux lignes de cache, le mécanisme ne marche pas. Rien n'indique que la seconde ligne de cache est présente dans le cache. Et si elle l'est, difficile de savoir où elle est dans le cache.
===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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===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===
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. En général, 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.
Une 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 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 superscalaires 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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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 macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour 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. Elle 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 une unité 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
sxvxyb5rdwpggnsovw6hf32ybl2lacw
772922
772921
2026-09-22T22:36:11Z
Mewtow
31375
/* Le chargement de plusieurs instructions consécutives */
772922
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. La séparation entre les deux a eu lieu approximativement dans les années 90. Durant cette décennie, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. En conséquence, ils ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques. Il y avait une certaine continuité dans les CPU historiques, qui ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===L'émission parallèle des branchements===
La double émission entière-flottante était l'implémentation la plus simple qui soit. L'étape suivante visait elle aussi à exploiter les unités de calcul déjà présentes dans le processeur. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle des autres instructions. Combinée à la double émission entière-flottante, cela permettait d'émettre en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les branchements étant fréquents sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601'').
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, reliée au registre d'état et au séquenceur. 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 utilise 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 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===L'émission multiple des micro-opérations entières===
Le troisième type de CPU superscalaire historique 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. Les 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est ppossible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui 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. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques l'utilisaient, quelques CPU des années 70-80, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==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. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
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 à l'émission", et détecte les dépendances entre avec les µops en vol, 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 vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
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.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. 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 n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, 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 micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. 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-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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.
Charger plusieurs instructions consécutives 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 ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement 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.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
===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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===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===
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. En général, 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.
Une 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 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 superscalaires 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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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 macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour 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. Elle 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 une unité 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
sad9z2hp36xnjwsewf9qej0givucsse
772923
772922
2026-09-22T22:36:23Z
Mewtow
31375
/* L'étape de chargement d'un CPU superscalaire large */
772923
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. La séparation entre les deux a eu lieu approximativement dans les années 90. Durant cette décennie, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. En conséquence, ils ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques. Il y avait une certaine continuité dans les CPU historiques, qui ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===L'émission parallèle des branchements===
La double émission entière-flottante était l'implémentation la plus simple qui soit. L'étape suivante visait elle aussi à exploiter les unités de calcul déjà présentes dans le processeur. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle des autres instructions. Combinée à la double émission entière-flottante, cela permettait d'émettre en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les branchements étant fréquents sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601'').
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, reliée au registre d'état et au séquenceur. 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 utilise 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 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===L'émission multiple des micro-opérations entières===
Le troisième type de CPU superscalaire historique 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. Les 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est ppossible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui 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. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques l'utilisaient, quelques CPU des années 70-80, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==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. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
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 à l'émission", et détecte les dépendances entre avec les µops en vol, 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 vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
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.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. 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 n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, 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 micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. 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-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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.
Charger plusieurs instructions consécutives 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 ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement 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.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
===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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===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===
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. En général, 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.
Une 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 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 superscalaires 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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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''.
===Les chargements de blocs superscalaires non-alignés===
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instruction même s'il n'est pas aligné. Le cache d'instruction de ce processeur est un cache à deux voies, où une ligne de cache d'instruction contient exactement 16 instructions. Chaque voie est composée de 4 SRAM sont numérotées 1, 2, 3 et 4. 4 instructions consécutives sont placées dans 4 SRAMs, avec une instruction par SRAM. Le chargement d'un bloc aligné demande juste de lire les 4 SRAMs en parallèle et de concaténer le résultat.
Le chargement d'un bloc aligné demande lui de faire quelques trucs en plus. Imaginons qu'on charge 4 instructions non-alignées, la première étant en troisième place d'un bloc. La première instruction est donc dans la SRAM 3, le suivant dans la 4, puis on revient à la SRAM 1 puis 2. Il est donc possible de charger les quatre instructions en même temps. Par contre, pour les deux dernières instructions, lues dans la SRAM 1 et 2, on doit lire les instructions qui sont une adresse après ! On veut lire les adresses après le bloc, pas avant. Des circuits détectent cette situation en regardant les bits de poids faible de l'adresse et l'incrémentent si besoin. De plus, il faut remettre les instructions dans l'ordre. Ce qui est fait par un réseau de multiplexeurs.
Notez que cette technique marche si les instructions sont alignées sur une ligne de cache. Si une instruction est à cheval sur deux lignes de cache, le mécanisme ne marche pas. Rien n'indique que la seconde ligne de cache est présente dans le cache. Et si elle l'est, difficile de savoir où elle est dans le cache.
===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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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 macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour 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. Elle 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 une unité 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
n8ocwcd1z9l5z5b25agl1xwijbou63g
772924
772923
2026-09-22T22:37:22Z
Mewtow
31375
/* Les chargements de blocs superscalaires non-alignés */
772924
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. La séparation entre les deux a eu lieu approximativement dans les années 90. Durant cette décennie, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. En conséquence, ils ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques. Il y avait une certaine continuité dans les CPU historiques, qui ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===L'émission parallèle des branchements===
La double émission entière-flottante était l'implémentation la plus simple qui soit. L'étape suivante visait elle aussi à exploiter les unités de calcul déjà présentes dans le processeur. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle des autres instructions. Combinée à la double émission entière-flottante, cela permettait d'émettre en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les branchements étant fréquents sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601'').
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, reliée au registre d'état et au séquenceur. 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 utilise 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 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===L'émission multiple des micro-opérations entières===
Le troisième type de CPU superscalaire historique 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. Les 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est ppossible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui 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. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques l'utilisaient, quelques CPU des années 70-80, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==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. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
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 à l'émission", et détecte les dépendances entre avec les µops en vol, 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 vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
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.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. 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 n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, 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 micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. 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-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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.
Charger plusieurs instructions consécutives 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 ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement 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.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
===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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===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===
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. En général, 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.
Une 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 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 superscalaires 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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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 macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour 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. Elle 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 une unité 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
sad9z2hp36xnjwsewf9qej0givucsse
772925
772924
2026-09-22T22:37:35Z
Mewtow
31375
/* Le chargement de plusieurs instructions consécutives */
772925
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. La séparation entre les deux a eu lieu approximativement dans les années 90. Durant cette décennie, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. En conséquence, ils ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques. Il y avait une certaine continuité dans les CPU historiques, qui ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===L'émission parallèle des branchements===
La double émission entière-flottante était l'implémentation la plus simple qui soit. L'étape suivante visait elle aussi à exploiter les unités de calcul déjà présentes dans le processeur. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle des autres instructions. Combinée à la double émission entière-flottante, cela permettait d'émettre en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les branchements étant fréquents sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601'').
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, reliée au registre d'état et au séquenceur. 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 utilise 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 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===L'émission multiple des micro-opérations entières===
Le troisième type de CPU superscalaire historique 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. Les 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est ppossible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui 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. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques l'utilisaient, quelques CPU des années 70-80, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==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. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
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 à l'émission", et détecte les dépendances entre avec les µops en vol, 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 vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
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.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. 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 n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, 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 micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. 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-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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.
Charger plusieurs instructions consécutives 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 ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement 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.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
===Les chargements de blocs superscalaires non-alignés===
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instruction même s'il n'est pas aligné. Le cache d'instruction de ce processeur est un cache à deux voies, où une ligne de cache d'instruction contient exactement 16 instructions. Chaque voie est composée de 4 SRAM sont numérotées 1, 2, 3 et 4. 4 instructions consécutives sont placées dans 4 SRAMs, avec une instruction par SRAM. Le chargement d'un bloc aligné demande juste de lire les 4 SRAMs en parallèle et de concaténer le résultat.
Le chargement d'un bloc aligné demande lui de faire quelques trucs en plus. Imaginons qu'on charge 4 instructions non-alignées, la première étant en troisième place d'un bloc. La première instruction est donc dans la SRAM 3, le suivant dans la 4, puis on revient à la SRAM 1 puis 2. Il est donc possible de charger les quatre instructions en même temps. Par contre, pour les deux dernières instructions, lues dans la SRAM 1 et 2, on doit lire les instructions qui sont une adresse après ! On veut lire les adresses après le bloc, pas avant. Des circuits détectent cette situation en regardant les bits de poids faible de l'adresse et l'incrémentent si besoin. De plus, il faut remettre les instructions dans l'ordre. Ce qui est fait par un réseau de multiplexeurs.
Notez que cette technique marche si les instructions sont alignées sur une ligne de cache. Si une instruction est à cheval sur deux lignes de cache, le mécanisme ne marche pas. Rien n'indique que la seconde ligne de cache est présente dans le cache. Et si elle l'est, difficile de savoir où elle est dans le cache.
===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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===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===
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. En général, 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.
Une 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 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 superscalaires 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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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 macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour 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. Elle 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 une unité 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
q1wx983ijlu16iijo07nrgirse35su1
772926
772925
2026-09-22T23:08:30Z
Mewtow
31375
/* Les chargements de blocs superscalaires non-alignés */
772926
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. 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. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[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.
==Le nombre de voies d'un processeur superscalaire==
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. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. 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. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. La séparation entre les deux a eu lieu approximativement dans les années 90. Durant cette décennie, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. En conséquence, ils ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques. Il y avait une certaine continuité dans les CPU historiques, qui ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
===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'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, 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 l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. 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]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===L'émission parallèle des branchements===
La double émission entière-flottante était l'implémentation la plus simple qui soit. L'étape suivante visait elle aussi à exploiter les unités de calcul déjà présentes dans le processeur. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle des autres instructions. Combinée à la double émission entière-flottante, cela permettait d'émettre en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les branchements étant fréquents sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601'').
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, reliée au registre d'état et au séquenceur. 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 utilise 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 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é de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===L'émission multiple des micro-opérations entières===
Le troisième type de CPU superscalaire historique 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. Les 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.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
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 ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est ppossible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui 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. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. 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 est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques l'utilisaient, quelques CPU des années 70-80, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==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. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les 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 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
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 à l'émission", et détecte les dépendances entre avec les µops en vol, 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 vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
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.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. 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 n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB 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éparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, 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 micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. 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-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
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. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|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 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.
Charger plusieurs instructions consécutives 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 ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement 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.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===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 ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit 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.
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. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. 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. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. 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.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. 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.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils 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'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges 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. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir 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, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première 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
La seconde 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 exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui 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.
Quelques processeurs utilisaient des solutions intermédiaires. 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 classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
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 d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture 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 d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
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 un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée 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. 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.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===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===
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. En général, 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.
Une 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 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 superscalaires 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 instructions 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 auto-aligné.]]
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 auto-aligné.]]
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 longs, 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 micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture 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 instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées 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 processeurs superscalaires 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 macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour 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. Elle 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 une unité 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. À 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 instructions ''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ées 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 micro-architectures 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 micro-architecture 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 micro-architecture 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>
of8vh6f40ocmiyjewddvukjszp3jar4
Mathc initiation/006x
0
84505
772822
772151
2026-09-22T14:08:32Z
Xhungab
23827
772822
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/006s#Quelques exercices II|Sommaire]]
'''La transformée de Laplace d'une intégrale : ''' '''L{Int F(u) du} = (1/s) f(s)'''
'''Remarque :'''
L{sin(t)} = 1/(s^2+1) ... L{sin(at)} = a/(s^2+a^2)
L{cos(t)} = s/(s^2+1) ... L{cos(at)} = s/(s^2+a^2)
L{sinh(t)} = 1/(s^2-1) ... L{sinh(at)} = a/(s^2-a^2)
L{cosh(t)} = s/(s^2-1) ... L{cosh(at)} = s/(s^2-a^2)
'''a) Calculons la transformée de Laplace de :''' L{Int F(u) du} = (1/s) f(s) )
L{Int sin(a u) du} = (1/s) a/(s^2+a^2)
'''b) Calculons la transformée de Laplace de :''' L{Int F(u) du} = (1/s) f(s) )
L{Int cos(a u) du} = (1/s) s/s^2+a^2
'''c) Calculons la transformée de Laplace de :''' L{Int F(u) du} = (1/s) f(s) )
L{Int sinh(a u) du} = (1/s) a/(s^2-a^2)
'''d) Calculons la transformée de Laplace de :''' L{Int F(u) du} = (1/s) f(s) )
L{Int cosh(a u) du} = (1/s) s/(s^2-a^2)
{{AutoCat}}
23ffl4rsgt5d227s80x7hnxw1labb2y
Mathc initiation/0073
0
84519
772819
772814
2026-09-22T14:06:18Z
Xhungab
23827
772819
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a522#Analyse IV : Se familiariser avec la Transformée de Laplace|Sommaire]]
{{Partie{{{type|}}}|La transformée en Z : Quelques astuces}}
La fonction δ[n] est l'impulsion de Kronecker, qui vaut 1 pour n = 0 et 0 ailleurs.
Pour un signal causal on utilise :
* z <--> δ[n+1] * 1 <--> δ[n]
* z^2 <--> δ[n+2] * 5 <--> 5 δ[n]
* z^k <--> δ[n+k] * C <--> C δ[n]
Le cas : 1/(z-a)
Pour résoudre le problème, il faut introduire un "z" au numérateur. Les transformées en "Z" ont, en générales un "z" au numérateur.
On peut utiliser :
1/(z-a) = 1/a (z/(z-a) - 1)
Vérifions :
1 z 1 z (z-a) 1 z - z+a 1 a 1
- (--- - 1) = - (--- - -----) = - (-------) = - (---) = ---
a z-a a z-a z-a a z-a a z-a z-a
z^2+4z+3
Soit : --------
z^2-4z-3
Effectuez l'opération :
z^2-4z-3
________ Quotient = 1
z^2+4z+3 | 1 Reste = 8z+6
- (z^2-4z-3)
----------
0+8z+6
z^2+4z+3 8z+6
Soit : -------- = 1 + --------
z^2-4z-3 z^2-4z-3
{{AutoCat}}
fpa5x49ynhm9xx9iqz28qeciariqnas
772820
772819
2026-09-22T14:06:48Z
Xhungab
23827
772820
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a522#Analyse IV : Se familiariser avec la Transformée de Laplace|Sommaire]]
{{Partie{{{type|}}}|La transformée en Z : Quelques astuces}}
La fonction δ[n] est l'impulsion de Kronecker, qui vaut 1 pour n = 0 et 0 ailleurs.
Pour un signal causal on utilise :
* z <--> δ[n+1] * 1 <--> δ[n]
* z^2 <--> δ[n+2] * 5 <--> 5 δ[n]
* z^k <--> δ[n+k] * C <--> C δ[n]
Le cas : 1/(z-a)
Pour résoudre le problème, il faut introduire un "z" au numérateur. Les transformées en "Z" ont, en générales un "z" au numérateur.
On peut utiliser :
1/(z-a) = 1/a (z/(z-a) - 1)
Vérifions :
1 z 1 z (z-a) 1 z - z+a 1 a 1
- (--- - 1) = - (--- - -----) = - (-------) = - (---) = ---
a z-a a z-a z-a a z-a a z-a z-a
z^2+4z+3
Soit : --------
z^2-4z-3
Effectuez l'opération :
z^2-4z-3
________ Quotient = 1
z^2+4z+3 | 1 Reste = 8z+6
- (z^2-4z-3)
----------
0+8z+6
z^2+4z+3 8z+6
Soit : -------- = 1 + --------
z^2-4z-3 z^2-4z-3
{{AutoCat}}
1m7nzlxujrxrmzn0b4hrzmhcvanrmsg